I use React Redux and I create a function to login, but I need to get a callback return after successfull login and redirect user to a page.
I try to passing function as parameter but not working.
How can I get the return after dispatch action?
Login fun
export const login = (request,cb) => {
return dispatch => {
let url = "/api/user/login";
axios({
method: "post",
url: url,
data: request,
config: { headers: { "Content-Type": "multipart/form-data" } }
})
.then(response => {
let authState = {
isLoggedIn: true,
user: response.data
};
cb();
window.localStorage["authState"] = JSON.stringify(authState);
return dispatch({
type: "USER_LOGIN_FULFILLED",
payload: { userAuthData: response.data }
});
})
.catch(err => {
return dispatch({
type: "USER_LOGIN_REJECTED",
payload: err
});
});
};
};
submiting
handleLogin(e) {
this.setState({ showLoader: true });
e.preventDefault();
const request = new Object();
if (this.validator.allValid()) {
request.email = this.state.email;
request.password = this.state.password;
this.props.login(request, () => {
//get callbach here
this.props.history.push('/my-space/my_views');
})
this.setState({ showLoader: false });
} else {
this.setState({ showLoader: false });
this.validator.showMessages();
this.forceUpdate();
}
}
const mapStateToProps = state => {
return {
authState: state
};
};
const mapDispatchToProps = dispatch => {
return {
login: request => dispatch(login(request))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
The cb is missing in your connect(...)
Here is the fix
handleLogin(e) {
this.setState({ showLoader: true });
e.preventDefault();
const request = new Object();
if (this.validator.allValid()) {
request.email = this.state.email;
request.password = this.state.password;
this.props.login(request, () => {
//get callbach here
this.props.history.push('/my-space/my_views');
})
this.setState({ showLoader: false });
} else {
this.setState({ showLoader: false });
this.validator.showMessages();
this.forceUpdate();
}
}
const mapStateToProps = state => {
return {
authState: state
};
};
const mapDispatchToProps = dispatch => {
return {
login: (request, cb) => dispatch(login(request, cb))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
Hope it helps:)
If you are using redux-thunk, you can return a Promise from your async action.
The function called by the thunk middleware can return a value,
that is passed on as the return value of the dispatch method.
In this case, we return a promise to wait for.
This is not required by thunk middleware, but it is convenient for us.
But I prefer use useEffect or componentDidUpdate for this purpose:
componentDidUpdate(){
if(this.props.authState.isLoggedIn){
this.props.history.push('/my-space/my_views');
}
}
I recommend using the Redux Cool package if you need actions with callback capability.
Instalation
npm install redux-cool
Usage
import {actionsCreator} from "redux-cool"
const my_callback = () => {
console.log("Hello, I am callback!!!")
}
const callbackable_action = actionsCreator.CALLBACKABLE.EXAMPLE(1, 2, 3, my_callback)
console.log(callbackable_action)
// {
// type: "CALLBACKABLE/EXAMPLE",
// args: [1, 2, 3],
// cb: f() my_callback,
// _index: 1
// }
callbackable_action.cb()
// "Hello, I am callback!!!"
When we try to generate an action object, we can pass the callback function as the last argument. actionsCreator will check and if the last argument is a function, it will be considered as a callback function.
See Actions Creator for more details
react-redux/redux dispatch returns a promise. you can do this if you want to return a value or identify if the request is success/error after being dispatched
Action example
export const fetchSomething = () => async (dispatch) => {
try {
const response = await fetchFromApi();
dispatch({
type: ACTION_TYPE,
payload: response.value
});
return Promise.resolve(response.value);
} catch (error) {
return Promise.reject(error);
}
}
Usage
const foo = async data => {
const response = new Promise((resolve, reject) => {
dispatch(fetchSomething())
.then(v => resolve(v))
.catch(err => reject(err))
});
await response
.then((v) => navigateToSomewhere("/", { replace: true }))
.catch(err => console.log(err));
};
this post is old, but hopefully it will help
Package.json
"react-redux": "^8.0.2"
"#reduxjs/toolkit": "^1.8.5"
Related
I'm trying to export an array inside a .then statement but its not working. I have no clue how to make it work otherwise. Actually I'm just trying to set my initial state in redux to this static data I am receiving from the movie database api.
import { API_URL, API_KEY } from '../Config/config';
const urls = [
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=1`,
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=2`,
]
Promise.all(urls.map(items => {
return fetch(items).then(response => response.json())
}))
.then(arrayOfObjects => {
var arr1 = arrayOfObjects[0].results;
var arr2 = arrayOfObjects[1].results;
export var movieData = arr1.concat(arr2);
}
)
You can try with a function. like this:
import { API_URL, API_KEY } from '../Config/config';
export const getMovies = () => {
const urls = [
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=1`,
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=2`,
]
const promises = urls.map(url => {
return new Promise((reject, resolve) => {
fetch(url).then(res => res.json())
.then(res => resolve(res.results))
})
})
return Promise.all(promises)
}
// other file
import {getMovies} from 'YOUR_API_FILE.js';
getMovies().then(moviesArr => {
// your business logics here
})
It's not clear where this code is in relation to your state/reducer, but ideally you should be using action creators to deal with any API calls and dispatch state updates, and those action creators can be called from the component.
So, initialise your state with an empty array:
const initialState = {
movies: []
};
Set up your reducer to update the state with MOVIES_UPDATE:
function reducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case 'MOVIES_UPDATE': {
return { ...state, movies: payload };
}
}
}
You can still use your function for fetching data:
function fetchData() {
return Promise.all(urls.map(items => {
return fetch(items).then(response => response.json());
}));
}
..but it's called with an action creator (it returns a function with dispatch param), and this action creator 1) gets the data, 2) merges the data, 3) and dispatches the data to the store.
export function getMovies() {
return (dispatch) => {
fetchData().then(data => {
const movieData = data.flatMap(({ results }) => results);
dispatch({ type: 'MOVIES_UPDATE', payload: movieData });
});
}
}
And it's called from within your component like so:
componentDidMount () {
this.props.dispatch(getMovies());
}
You can modify the code as below:
import { API_URL, API_KEY } from '../Config/config';
let movieData='';
exports.movieData = await (async function(){
const urls = [
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=1`,
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=2`,
];
const arrayOfObjects = await Promise.all(urls.map(items => {
return fetch(items).then(response => response.json())
}));
return arrayOfObjects[0].results.concat(arrayOfObjects[1].results);
})();
I have this React-Native app, which fetches a list of items, and then, an image gallery for each of those items. So basically, I have two ajax function, and the second one needs the list of items fetched in the first function.
I make the first invoke in componentDidMount(), but I don't know how to "wait" for it to finish to make the second call. If I just place them one after the other, the second one won't do anything because there is no list of items yet.
How can I solve this? I read similar questions here on StackOverflow but couldn't find a clear example for this case. Also, in the code, I commented some of my unsuccessful tries.
Thanks in advance, I'm new to React so I can be missing something simple.
component.js
class EtiquetasList extends Component {
componentDidMount() {
this.props.FetchEtiquetas();
if ( this.props.etiquetas.length > 0 ) { // I tried this but didn't do anything
this.props.FetchGalleries( this.props.etiquetas );
}
}
renderEtiquetas() {
if ( this.props.galleries.length > 0 ) {
// if I invoke FetchGalleries here, it works, but loops indefinitely
return this.props.etiquetas.map(etiqueta =>
<EtiquetaDetail key={etiqueta.id} etiqueta={etiqueta} galleries={ this.props.galleries } />
);
}
}
render() {
return (
<ScrollView>
{ this.renderEtiquetas() }
</ScrollView>
);
}
}
const mapStateToProps = (state) => {
return {
etiquetas: state.data.etiquetas,
isMounted: state.data.isMounted,
galleries: state.slides.entries
};
};
export default connect(mapStateToProps, { FetchEtiquetas, FetchGalleries })(EtiquetasList);
actions.js
export function FetchEtiquetas() {
return function (dispatch) {
axios.get( url )
.then(response => {
dispatch({ type: FETCH_ETIQUETAS_SUCCESS, payload: response.data })
}
);
}
}
export function FetchGalleries( etiquetas ) {
return function (dispatch) {
return Promise.all(
etiquetas.map( record =>
axios.get('https://e.dgyd.com.ar/wp-json/wp/v2/media?_embed&parent='+record.id)
)).then(galleries => {
let curated_data = [];
let data_json = '';
galleries.map( record => {
record.data.map( subrecord => {
// this is simplified for this example, it works as intended
data_json = data_json + '{ title: "' + subrecord.title+'"}';
});
my_data.push( data_json );
});
return dispatch({ type: FETCH_GALLERIES_SUCCESS, payload: curated_data });
});
}
}
reducer.js
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case FETCH_ETIQUETAS_SUCCESS:
return { ...state, etiquetas: action.payload, isMounted: true }; // I think it ran faster like this tho
default:
return state;
}
};
You may try modifying your "Fetch" functions to return the Promise object. Then you can chain them togather like this:
export function FetchEtiquetas() {
return function (dispatch) {
return axios.get( url ).then(response => {
dispatch({ type: FETCH_ETIQUETAS_SUCCESS, payload: response.data })
})
}
}
Invoke in "componentDidMount":
this.props.FetchEtiquetas().then(() => {
this.props.FetchGalleries( this.props.etiquetas );
})
componentWillReceiveProps(nextProps) {
if(nextProps.etiquetas.length !== 0 && this.props.etiquetas.length === 0) {
this.props.FetchGalleries( nextProps.etiquetas );
}
}
I think componentWillReceiveProps lifecycle can do the job easily. You should use componentWillReceiveProps whenever you need to trigger something in update phase of react components
To chain your actions, I would recommend chaining Promise or using async/await if you can.
To do it with Promises, you should return the Promise returned by the request in the action.
export function FetchEtiquetas() {
return function (dispatch) {
return axios.get( url )
.then(response => {
dispatch({ type: FETCH_ETIQUETAS_SUCCESS, payload: response.data })
}
);
}
}
And use mapDispatchToProps on redux connect call, it should look like
function mapDispatchToProps(dispatch) {
return {
fetchData: () => {
dispatch(FetchEtiquetas)
.then((results) => {
dispatch(FetchGalleries(results))
});
}
}
}
Then in your component, just call the right prop in componentDidMount
componentDidMount() {
this.props.fetchData();
}
From your code I assume you are using redux-thunk.
I suggest combining them together to an action creator like this (which is possible in redux-thunk):
export function FetchEtiquetasThenGalleries() {
return function (dispatch) {
axios.get(url)
.then(response => {
dispatch({ type: FETCH_ETIQUETAS_SUCCESS, payload: response.data })
const etiquetas = response.data; // Assuming response.data is that array.
return Promise.all(
etiquetas.map(record =>
axios.get('https://e.dgyd.com.ar/wp-json/wp/v2/media?_embed&parent=' + record.id)
)).then(galleries => {
let curated_data = [];
let data_json = '';
galleries.map(record => {
record.data.map(subrecord => {
// this is simplified for this example, it works as intended
data_json = data_json + '{ title: "' + subrecord.title + '"}';
});
my_data.push(data_json);
});
dispatch({ type: FETCH_GALLERIES_SUCCESS, payload: curated_data });
});
});
}
}
In my React app I'm trying to fake a delay in mock API response
Uncaught TypeError: Cannot read property 'then' of undefined
createDraft() {
// Post to Mock API
const newDraft = {
name: this.state.draftName,
created_by: this.props.user,
created: getDate()
};
/* ERROR HERE */
this.props.create(newDraft).then((res) => {
console.log(' res', res);
// Display loading spinner while waiting on Promise
// Close modal and redirect to Draft Summary view
this.props.closeModal();
});
}
The action:
export const create = newDraft => dispatch => all()
.then(() => setTimeout(() => {
dispatch({
type: CREATE,
newDraft
});
return 'Draft created!';
}, 2000))
.catch(() => {
dispatch({
type: REQUEST_ERROR,
payload: apiErrors.BAD_REQUEST
});
});
Connected area:
const mapDispatchToProps = dispatch => ({
create: (newDraft) => { dispatch(create(newDraft)); }
});
export const CreateDraftModalJest = CreateDraftModal;
export default connect(cleanMapStateToProps([
'user'
]), mapDispatchToProps)(CreateDraftModal);
Also tried this with same result
function DelayPromise(t) {
return new Promise(((resolve) => {
setTimeout(resolve, t);
}));
}
export const create = newDraft => dispatch => all()
.then(DelayPromise(2000))
.then(() => {
console.log('created called', newDraft);
dispatch({
type: CREATE,
newDraft
});
})
.then(() => 'Draft created!')
.catch(() => {
dispatch({
type: REQUEST_ERROR,
payload: apiErrors.BAD_REQUEST
});
});
It's possible that your this.props.create() action creator is not bound to dispatch, and therefore not returning the promise as expected.
Are you using react-redux connect() properly?
EDIT:
Your mapDispatchToProps is not returning the promise. Do this instead:
const mapDispatchToProps = dispatch => ({
create: (newDraft) => dispatch(create(newDraft)),
});
ORIGINAL QUESTION
I'm following the example for writing tests for async action creators spelled out in the Redux documentation. I'm following the example as closely as possible, but I can't get the test to work. I'm getting the following error message:
TypeError: Cannot read property 'then' of undefined
(node:789) UnhandledPromiseRejectionWarning: Unhandled promise rejection
(rejection id: 28): TypeError: Cannot read property 'data' of undefined
Here is the code for my action creator and test:
actions/index.js
import axios from 'axios';
import { browserHistory } from 'react-router';
import { AUTH_USER, AUTH_ERROR, RESET_AUTH_ERROR } from './types';
const API_HOST = process.env.NODE_ENV == 'production'
? http://production-server
: 'http://localhost:3090';
export function activateUser(token) {
return function(dispatch) {
axios.put(`${API_HOST}/activations/${token}`)
.then(response => {
dispatch({ type: AUTH_USER });
localStorage.setItem('token', response.data.token);
})
.catch(error => {
dispatch(authError(error.response.data.error));
});
}
}
export function authError(error) {
return {
type: AUTH_ERROR,
payload: error
}
}
confirmation_test.js
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import * as actions from '../../src/actions';
import { AUTH_USER, AUTH_ERROR, RESET_AUTH_ERROR } from
'../../src/actions/types';
import nock from 'nock';
import { expect } from 'chai';
const middlewares = [ thunk ];
const mockStore = configureMockStore(middlewares);
describe('Confirmation_Token action creator', () => {
afterEach(() => {
nock.cleanAll()
});
it('dispatches AUTH_USER', (done) => {
nock('http://localhost:3090')
.put('/activations/123456')
.reply(200, {
token: 7891011
});
const expectedActions = { type: AUTH_USER };
const store = mockStore({});
return store.dispatch(actions.activateUser(123456))
.then(() => { // return of async actions
expect(store.getActions()).toEqual(expectedActions);
done();
});
});
});
UPDATED QUESTION
I've partially (though not entirely) figured this out. I got this to work by adding a return statement in front of axios and commenting out the localstorage.setItem call.
I also turned the object I assigned to expectedActions to an array, and changed my assertion from toEqual to to.deep.equal. Here is the modified code:
actions/index.js
export function activateUser(token) {
return function(dispatch) { // added return statement
return axios.put(`${API_HOST}/activations/${token}`)
.then(response => {
dispatch({ type: AUTH_USER });
// localStorage.setItem('token', response.data.token); Had to comment out local storage
})
.catch(error => {
dispatch(authError(error.response.data.error));
});
}
}
confirmation_test.js
describe('ConfirmationToken action creator', () => {
afterEach(() => {
nock.cleanAll()
});
it('dispatches AUTH_USER', (done) => {
nock('http://localhost:3090')
.put('/activations/123456')
.reply(200, {
token: 7891011
});
const expectedActions = [{ type: AUTH_USER }];
const store = mockStore({});
return store.dispatch(actions.activateUser(123456))
.then(() => { // return of async actions
expect(store.getActions()).to.deep.equal(expectedActions);
done();
});
});
});
But now I can't test localStorage.setItem without producing this error message:
Error: timeout of 2000ms exceeded. Ensure the done() callback is being called
in this test.
Is this because I need to mock out localStorage.setItem? Or is there an easier solve that I'm missing?
I figured out the solution. It involves the changes I made in my updated question as well as adding a mock of localStorage to my test_helper.js file. Since there seems to be a lot of questions about this online, I figured perhaps my solution could help someone down the line.
test_helper.js
import jsdom from 'jsdom';
global.localStorage = storageMock();
global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
global.window = global.document.defaultView;
global.navigator = global.window.navigator;
global.window.localStorage = global.localStorage;
// localStorage mock
function storageMock() {
var storage = {};
return {
setItem: function(key, value) {
storage[key] = value || '';
},
getItem: function(key) {
return key in storage ? storage[key] : null;
},
removeItem: function(key) {
delete storage[key];
}
};
}
actions.index.js
export function activateUser(token) {
return function(dispatch) {
return axios.put(`${API_HOST}/activations/${token}`)
.then(response => {
dispatch({ type: AUTH_USER });
localStorage.setItem('token', response.data.token);
})
.catch(error => {
dispatch(authError(error.response.data.error));
});
}
}
confirmation_test.js
describe('Confirmation action creator', () => {
afterEach(() => {
nock.cleanAll()
});
it('dispatches AUTH_USER and stores token in localStorage', (done) => {
nock('http://localhost:3090')
.put('/activations/123456')
.reply(200, {
token: '7891011'
});
const expectedActions = [{ type: AUTH_USER }];
const store = mockStore({});
return store.dispatch(actions.activateUser(123456))
.then(() => { // return of async actions
expect(store.getActions()).to.deep.equal(expectedActions);
expect(localStorage.getItem('token')).to.equal('7891011');
done();
});
});
});
I have the following debounced function that gets called every time a user inputs into the username field. It is working as expected.
export const uniqueUsernameCheck = _.debounce(({ username }) => {
axios.post(`${API_URL}/signup/usernamecheck`, { username })
.then((res) => {
console.log('Is unique?', res.data.status);
})
.catch((error) => {
console.log(error);
});
}, 500);
However using redux-thunk I am trying to modify the function so that I can dispatch actions within my function. This is what I have:
export const uniqueUsernameCheck = _.debounce(({ username }) => {
console.log('I can see this');
return (dispatch) => {
console.log('But not this');
dispatch({ type: USERNAME_CHECK });
axios.post(`${API_URL}/signup/usernamecheck`, { username })
.then((res) => {
dispatch(authError(res.data.error));
})
.catch((error) => {
console.log(error);
});
};
}, 500);
The problem lies in that the above code no longer fires off my post request like the initial function did and nothing ever gets dispatched. I know I'm doing something wrong but can't figure out what.
EDIT:
This is how I've set up my store
const store = createStore(reducers, {}, applyMiddleware(ReduxThunk));
Take a look at this:
http://codepen.io/anon/pen/egeOyJ
const userService = _.debounce(username => {
setTimeout(
()=>{
console.log('userService called after debounce. username:', username)
}
,1000)
}, 500)
const uniqueUsernameCheck = (username) => (dispatch) => {
console.log('I can see this')
userService(username)
}
console.log('begin')
const reducers = (action) => {console.log(action)}
const store = Redux.createStore(
reducers,
{},
Redux.applyMiddleware(ReduxThunk.default))
store.dispatch(uniqueUsernameCheck('rafael'))
store.dispatch(uniqueUsernameCheck('rafael'))
store.dispatch(uniqueUsernameCheck('rafael'))