Redux: How do I import the dispatch function? - react-native

I'm trying to call dispatch in a non-component file.
My issue is that I'm trying to use redux-saga, but it is not letting me use the yield keyword inside of a callback function that I have to define:
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
yield put({ type: videoSessionActions.SEND_LOCAL_CANDIDATE, payload: event.candidate });
}
}
So what I want to do instead is using plain old dispatch like so:
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
dispatch({ type: videoSessionActions.SEND_LOCAL_CANDIDATE, payload: event.candidate })
}
}
Is there a way to import { dispatch } from 'redux'; ?
BTW this is all happening in my generator function in my saga. The reason I am not using redux-observable is because that requires react native v0.40.0+ which I can't update yet

Sure, there are many ways to go about doing this, in this issue you can see how.
The main thing you have to do is connect your component to the store like this:
export default connect()(Controls); You may also provide a first argument to the connect function which will be the mapStateToProps function. If you do this, you will be able to do something like this outside your component (Globally)
let createHandlers = function(dispatch) {
let peerConnection.onicecandidate = function(event) {
if (event.candidate) {
dispatch({ type: videoSessionActions.SEND_LOCAL_CANDIDATE, payload: event.candidate })
};
}
return {
peerConnection.onicecandidate,
// other handlers
};
}
Cheers!

Related

Access component's this inside firebase promise reposonse

I've tried to bind it like it doesn't seem to make the trick :)
firebaseInstance.auth().fetchSignInMethodsForEmail(this.signUpData.email)
.then((response) => {
... all logic
}).bind(this)
...since it outputs the following error:
firebaseInstance.auth(...).fetchSignInMethodsForEmail(...).bind is not a function
Here is the component's logic, can someone please suggest a proper way to access this after firebase response resolves? :bowing:
import { VALIDATION_MESSAGES, VALUES } from './signup.module.config'
import GLOBAL_EVENTS from 'values/events'
import { firebaseInstance } from 'database'
export default {
name: `SignUpForm`,
data() {
return {
signUpData: {
email: ``,
password: ``,
confirmPassword: ``
}
}
},
methods: {
onEmailSignUp() {
// Here this is component
console.log(this.$refs)
firebaseInstance.auth().fetchSignInMethodsForEmail(this.signUpData.email)
.then((response) => {
// other logic
} else {
// Here this is lost and equals undefined
this.$refs.email.setCustomValidity(`error`)
}
})
}
}
}
The bind instruction should be used on a function object, not on a function return value.
By doing
firebaseInstance.auth().fetchSignInMethodsForEmail(this.signUpData.email)
.then((response) => {
... all logic
}).bind(this)
You try to use bind on the return of the then method of you promise, which is a promise object and can't use bind.
You can try firebaseInstance.auth().fetchSignInMethodsForEmail(this.signUpData.email)
.then(function(response){
... all logic
}.bind(this))
instead. Here the bind is put on the function send in the promise so it should work correctly. I also transformed the function from arrow function to normal, because I think there is no need for arrow function with bind.
Using ES8 async/await sugar syntax you can do it like this :
async onEmailSignUp () {
try {
const response = await firebaseInstance.auth().fetchSignInMethodsForEmail(this.signUpData.email)
// other logic
} catch (error) {
console.log(error)
this.$refs.email.setCustomValidity(`error`)
}
}

React redux async data sync then re-render view

I have a number of actions and reducers setup for different content types, e.g. pages, events and venues. These actions and reducers get data which has been saved to AsyncStorage, by another action called sync, and puts it into the store.
Sync performs an async call to Contentful and retrieves any new/updated/deleted entries, which I then save to AsyncStorage.
What is the best way to ensure the view correctly is re-rendered after the async call is finished?
Should syncReducer merge data into the store that would normally be pulled out by pagesReducer, venuesReducer etc or should there be some kind of event emitted after syncReducer is done?
Data is pulled in asynchronously for offline viewing and keeping things fast, so I really don't want to wait for the sync before rendering.
data/sync.js
import { AsyncStorage } from 'react-native';
import database from './database';
const cache = {
getByType: async (query) => {
return new Promise(async(resolve, reject) => {
// Get results from AsyncStorage
resolve(results);
});
},
sync: async () => {
return new Promise(async(resolve, reject) => {
database
.sync(options)
.then(async results => {
// Save results to AsyncStorage
resolve(results);
});
});
}
};
export default cache;
actions/sync.js
import actionTypes from '../constants/actionTypes';
import cache from '../data/cache';
export function sync() {
return dispatch => {
dispatch(syncRequestedAction());
return cache
.sync()
.then(() => {
dispatch(syncFulfilledAction());
})
.catch(error => {
console.log(error);
dispatch(syncRejectedAction());
});
};
}
function syncRequestedAction() {
return {
type: actionTypes.SyncRequested
};
}
function syncRejectedAction() {
return {
type: actionTypes.SyncRejected
};
}
function syncFulfilledAction(data) {
return {
type: actionTypes.SyncFulfilled,
data
};
}
actions/getPages.js
import actionTypes from '../constants/actionTypes';
import cache from '../data/cache';
export function getPages() {
return dispatch => {
dispatch(getPagesRequestedAction());
return cache
.getByType('page')
.then(results => {
dispatch(getPagesFulfilledAction(results));
})
.catch(error => {
console.log(error);
dispatch(getPagesRejectedAction());
});
};
}
function getPagesRequestedAction() {
return {
type: actionTypes.GetPagesRequested
};
}
function getPagesRejectedAction() {
return {
type: actionTypes.GetPagesRejected
};
}
function getPagesFulfilledAction(settings) {
return {
type: actionTypes.GetPagesFulfilled,
pages
};
}
reducers/pagesReducer.js
import { merge } from 'lodash';
import actionTypes from '../constants/actionTypes';
const pagesReducer = (state = {}, action) => {
switch (action.type) {
case actionTypes.GetPagesRequested: {
return merge({}, state, { loading: true });
}
case actionTypes.GetPagesRejected: {
return merge({}, state, { error: 'Error getting pages', loading: false });
}
case actionTypes.GetPagesFulfilled: {
const merged = merge({}, state, { error: false, loading: false });
return { ...merged, data: action.pages };
}
default:
return state;
}
};
export default pagesReducer;
In the end I was able to solve this by importing the other actions into my sync action, and dispatching depending on which data needs to be updated.
import { getEvents } from './getEvents';
import { getPages } from './getPages';
import { getVenues } from './getVenues';
export function sync() {
return dispatch => {
dispatch(syncRequestedAction());
return cache
.sync()
.then(results => {
dispatch(syncFulfilledAction());
if (results.includes('event')) {
dispatch(getEvents());
}
if (results.includes('page')) {
dispatch(getPages());
}
if (results.includes('venue')) {
dispatch(getSettings());
}
})
.catch(error => {
console.log(error);
dispatch(syncRejectedAction());
});
};
}
Your sync action should be a thunk function (redux middleware) that makes the call to Contentful, resolves the promise, and contains the data, or error. Then you can dispatch another action, or actions to reduce the data into the store.
On each component that you want to re-render (based on the data being updated in the store via the actions we just dispatched and reduced), if you have connect(mapStateToProps, mapDispatchToProps) and have included those parts of the store in MSTP, those props will be updated which will re-render the components.
You can even be more explicit about the resolution of data if necessary by creating another action where you can dispatch and reduce to some part of your store the current state of the fetch.
So when you make the call, you could dispatch 'FETCH_IN_PROGRESS', then either 'FETCH_ERROR' or 'FETCH_SUCCESS' and if that was mapStateToProps into your component, you could choose to evaluate it in shouldComponentUpdate() and based on where in the process it is, you could either return true or false based on if you wanted to rerender. You could also force render in componentWillReceiveProps. I'd start with just relying on props changing and adding this if necessary.
You should use Redux Persist for this kind of thing, it supports AsyncStorage and a range of other options.
https://github.com/rt2zz/redux-persist
Actions and Reducers should be just designed to update the Redux store. Any other action is known as a side effect, and should be managed in a Middleware or Store Enhancer.
I would strongly advise against using Redux-Thunk it is way too powerful for the few things that it is useful for and very easy to create unmaintainable anti-patten code as it blurs the boundaries between actions and middleware code.
If you think you need to use Redux-Thunk first look to see if their is already a middleware that does what you need and if not learn about Redux-Sagas.

how to `bindActionCreators` with redux-thunk

I am quite new to JavaScript and react-native and I have existing project that I need to add functionality to. It is using redux and redux-thunk with redux-saga to send API requests. Currently it supports only 1 dispatch function per component and I need to dispatch several types of requests to the saga. I am trying to bindActionCreators to add the dispatch to the stores but to no avail.. I am totally lost on the mapDispatchToProps part and how do I "fire the action" afterwards..
in single dispatch to props, I did this:
let sdtp = (arg) => {
return (dispatch) => {
dispatch({
type: 'GET_TEST_HASHMAP_SAGA',
hashmap: arg
})
}
}
export default MainPage = connect(
mapStateToProps,
{ sdtp }
)(MainPage);
and I can "access the function" (is this the right term? at least my saga gets called) inside the MainPage.render() component :
`this.props.sdtp({'hello':'world'});`
but when I change to use bindActionCreators, I cannot access it in the props anymore (I have tried so many different experiments I almost give up)
Here is how I construct my multiple dispatches:
let action1 = (args) => {
return (dispatch) => {
dispatch({
type: 'GET_TEST_HASHMAP_SAGA',
hashmap: arg
});
}
}
let action2 = (args) => {
return (dispatch) => {
dispatch({
type: 'GET_TEST_HASHMAP_SAGA2',
params: arg
});
}
}
let action3 = (args) => {
return (dispatch) => {
dispatch({
type: 'GET_TEST_HASHMAP_SAGA3',
args: arg
});
}
}
let mdtp = (dispatch) => {
return {
actions: bindActionCreators(action1, action2, action3, dispatch)
}
}
export default MainPage = connect(
mapStateToProps,
{ mdtp }
)(MainPage);
I am trying to access the actions like this:
this.props.mdtp.action1({arg: 'hello'});
Thanks in advance!
connect takes four arguments...most people usually only need the first two.
mapStateToProps you have, and I'm assuming it's a function.
mapDispatchToProps is the second...the issue is there.
bindActionCreators is nothing but a for loop...leave it out and you will better understand what is happening.
Try this:
function mapDispatchToProps(dispatch) {
return {
action1: (args) => dispatch(action1(args)),
action2: (args) => dispatch(action2(args)),
}
}
export default MainPageContainer = connect(
mapStateToProps,
mapDispatchToProps
)(MainPage)
And call them as
this.props.action1(args) and this.props.action2(args)
If you insist on using the overrated bindActionCreators the syntax would be:
function mapDispatchToProps(dispatch){
return {
actions: bindActionCreators({
action1,
action2,
}, dispatch)
}
}
Also, use const instead of let since you are not redefining the value. It is also best to export the connected component under a different name than the class name of the component.
In your mpdt function, you need to return result of bindActionCreators call, not object with action key.
So, it should be
const mdtp = (dispatch) => {
return bindActionCreators({
action1, action2, action3
}, dispatch);
};
and you can call them as this.props.action1(...)
From your code it also seems, that you have confused two ways of passing action creators to the component. One way is described above. And another way, you can pass your action creators directly to connect() using object notations, like so
export default MainPage = connect(
mapStateToProps,
{ action1, action2, action3 }
)(MainPage);
which will have same result. And your first approach, with sdtp action creator uses this approach.
Alternatively, you can also skip mapDispatchToProps entirely..
Within your render() function, you can just call dispatch directly like this:
this.props.dispatch({type: 'GET_TEST_HASHMAP_SAGA2', params: {"hello": "world"}});
Then in your connect function, you can skip the mapDispatchToProps param entirely.
export default MainPage = connect(
mapStateToProps
)(MainPage);
I know this isn't the answer, but this is just an alternative that works as well

How Can I pass params with an API client to vue-head?

I am passing params from my API to vue-head but every time I do that it send me undefined in the head this is the code:
export default {
data: () => ({
errors: [],
programs: [],
}),
methods: {
getProgram() {
this.api.http.get(`videos/program/${this.programSlug}`)
.then(response => {
this.programs = response.data
})
.catch(error => {
this.errors = error
});
}
},
head: {
title: function() {
return {
inner: this.programs.name,
separator: '|',
complement: 'Canal 10'
};
}
}
}
any idea what I am doing wrong with my code??
First verify you are fetching the information correctly. Use console log and go to network tab and verify you are fetching the data correct, you might have to comment out vue-head. But what I think is that the problem might be due to vue-head rendering before the api call finishes then no data is being passed.
If you are using vue-router this can be easily solved with beforeRouteEnter() hook. But if not! apparently vue-head has an event that you can emit to update the component after render.
I haven't tried this but it should work. you can add the function below to your methods and call it after the promise is resolved i.e in the then closure.
methods: {
getProgram() {
this.api.http.get(`videos/program/${this.programSlug}`)
.then(response => {
this.programs = response.data
this.$emit('updateHead')
})
.catch(error => {
this.errors = error
});
}
}

Unit testing HTTP request with Vue, Axios, and Mocha

I'm really struggling trying to test a request in VueJS using Mocha/Chai-Sinon, with Axios as the request library and having tried a mixture of Moxios and axios-mock-adaptor. The below examples are with the latter.
What I'm trying to do is make a request when the component is created, which is simple enough.
But the tests either complain about the results variable being undefined or an async timout.
Am I doing it right by assigning the variable of the getData() function? Or should Ireturn` the values? Any help would be appreciated.
Component
// Third-party imports
import axios from 'axios'
// Component imports
import VideoCard from './components/VideoCard'
export default {
name: 'app',
components: {
VideoCard
},
data () {
return {
API: '/static/data.json',
results: null
}
},
created () {
this.getData()
},
methods: {
getData: function () {
// I've even tried return instead of assigning to a variable
this.results = axios.get(this.API)
.then(function (response) {
console.log('then()')
return response.data.data
})
.catch(function (error) {
console.log(error)
return error
})
}
}
}
Test
import Vue from 'vue'
import App from 'src/App'
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
let mock = new MockAdapter(axios)
describe('try and load some data from somewhere', () => {
it('should update the results variable with results', (done) => {
console.log('test top')
mock.onGet('/static/data.json').reply(200, {
data: {
data: [
{ id: 1, name: 'Mexican keyboard cat' },
{ id: 2, name: 'Will it blend?' }
]
}
})
const VM = new Vue(App).$mount
setTimeout(() => {
expect(VM.results).to.be.null
done()
}, 1000)
})
})
I am not sure about moxios mock adaptor, but I had a similar struggle. I ended up using axios, and moxios, with the vue-webpack template. My goal was to fake retreiving some blog posts, and assert they were assigned to a this.posts variable.
Your getData() method should return the axios promise like you said you tried - that way, we have some way to tell the test method the promise finished. Otherwise it will just keep going.
Then inside the success callback of getData(), you can assign your data. So it will look like
return axios.get('url').then((response) {
this.results = response
})
Now in your test something like
it('returns the api call', (done) => {
const vm = Vue.extend(VideoCard)
const videoCard = new vm()
videoCard.getData().then(() => {
// expect, assert, whatever
}).then(done, done)
)}
note the use of done(). That is just a guide, you will have to modify it depending on what you are doing exactly. Let me know if you need some more details. I recommend using moxios to mock axios calls.
Here is a good article about testing api calls that helped me.
https://wietse.loves.engineering/testing-promises-with-mocha-90df8b7d2e35#.yzcfju3qv
So massive kudos to xenetics post above, who helped in pointing me in the right direction.
In short, I was trying to access the data incorrectly, when I should have been using the $data property
I also dropped axios-mock-adaptor and went back to using moxios.
I did indeed have to return the promise in my component, like so;
getData: function () {
let self = this
return axios.get(this.API)
.then(function (response) {
self.results = response.data.data
})
.catch(function (error) {
self.results = error
})
}
(Using let self = this got around the axios scope "problem")
Then to test this, all I had to do was stub the request (after doing the moxios.install() and moxios.uninstall for the beforeEach() and afterEach() respectively.
it('should make the request and update the results variable', (done) => {
moxios.stubRequest('./static/data.json', {
status: 200,
responseText: {
data: [
{ id: 1, name: 'Mexican keyboard cat' },
{ id: 2, name: 'Will it blend?' }
]
}
})
const VM = new Vue(App)
expect(VM.$data.results).to.be.null
VM.getData().then(() => {
expect(VM.$data.results).to.be.an('array')
expect(VM.$data.results).to.have.length(2)
}).then(done, done)
})