I'm facing weird behavior while fetching the records in redux saga. When I try to call an action in useeffect from the class, it is being called in loop multiple times. For that reason, there are infinite api calls.
Can anybody tell me where i'm wrong please? Following is my code.
Reducer
// #flow
import {
FETCH_DOCTOR_PROFILE,
FETCH_DOCTOR_PROFILE_SUCCESS,
FETCH_DOCTOR_PROFILE_ERROR
} from "./actionTypes";
const INIT_STATE = {
error: null,
isLoading: false,
doctorProfile: {}
};
const DoctorProfileReducer = (state = INIT_STATE, action) => {
switch (action.type) {
case FETCH_DOCTOR_PROFILE:
return {
...state,
isLoading: true
};
case FETCH_DOCTOR_PROFILE_SUCCESS:
return {
...state,
isLoading: false,
doctorProfile: action.payload
};
case FETCH_DOCTOR_PROFILE_ERROR:
return {
...state,
isLoading: false,
error: action.error
};
default:
return state;
}
};
export default DoctorProfileReducer;
Action
import {
FETCH_DOCTOR_PROFILE,
FETCH_DOCTOR_PROFILE_SUCCESS,
FETCH_DOCTOR_PROFILE_ERROR
} from "./actionTypes";
export const fetchDoctorProfileAction= () => ({
type: FETCH_DOCTOR_PROFILE
});
export const fetchDoctorProfileSuccessAction= (doctorProfile) => ({
type: FETCH_DOCTOR_PROFILE_SUCCESS,
payload: doctorProfile
});
export const fetchDoctorProfileErrorAction= (error) => ({
type: FETCH_DOCTOR_PROFILE_ERROR,
error: error
});
Saga
import { takeEvery, fork, put, all, call } from 'redux-saga/effects';
// Redux States
import { FETCH_DOCTOR_PROFILE } from './actionTypes';
import { fetchDoctorProfileSuccessAction, fetchDoctorProfileErrorAction } from './actions';
import { fetchDoctorProfileApi } from '../../../services/doctorProfile';
import {FETCH_DOCTOR_PROFILE_URL} from '../../../helpers/urls';
function* fetchDoctorProfileSaga() {
try {
const response = yield call(fetchDoctorProfileApi,FETCH_DOCTOR_PROFILE_URL);
yield put(fetchDoctorProfileSuccessAction(response));
} catch (error) {
yield put(fetchDoctorProfileErrorAction(error));
}
}
export function* watchFetchDoctorProfile() {
yield takeEvery(FETCH_DOCTOR_PROFILE, fetchDoctorProfileSaga)
}
function* doctorProfileSaga() {
yield all([
fork(watchFetchDoctorProfile),
]);
}
export default doctorProfileSaga;
Calling page
useEffect(() => {
props.fetchDoctorProfileAction();
const result = props.doctorProfile;
});
...........
const mapStateToProps = (state) => {
const { error, doctorProfile, pending } = state.DoctorProfileReducer;
return { error , doctorProfile, pending };
}
export default withRouter(connect(mapStateToProps, {fetchDoctorProfileAction})(ProfessionalProfilePrimary));
I think you need to add a condition in your useEffect() hook:
useEffect(() => {
if (!props.doctorProfile && !props.pending) {
props.fetchDoctorProfileAction();
}
const result = props.doctorProfile;
}, [props.doctorProfile, props.pending]);
NOTE: in your component mapStateToProps you have pending but in your store you have isLoading; make sure you are correctly mapping the state to the prop ;)
Related
I'm not sure why I'm forced to do a check if actions exists in my reducer. Could it be because we are using async await in our actions / API methods?
Reducer
export const partyReducer = (state = initState, action) => {
if (action) { // <-- should not need this
switch (action.type) {
case Actions.SET_ROLES: {
const roles = formatRoles(action.roles);
return {
...state,
roles
};
}
default:
return state;
}
}
return state;
};
export default partyReducer;
Actions
import {getRoles} from '../shared/services/api';
export const Actions = {
SET_ROLES: 'SET_ROLES'
};
export const fetchRoles = () => async dispatch => {
try {
const response = await getRoles();
const roles = response.data;
dispatch({
type: Actions.SET_ROLES,
roles
});
} catch (error) {
dispatch({
type: Actions.SET_ROLES,
roles: []
});
}
};
Component that dispatches the action:
componentDidMount() {
this.props.fetchRoles();
this.onSubmit = this.onSubmit.bind(this);
}
...
export const mapDispatchToProps = dispatch => {
return {
fetchRoles: () => {
dispatch(fetchRoles());
}
};
};
The Store
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
import thunk from 'redux-thunk';
import {reducer as formReducer} from 'redux-form';
// Reducers
import partyReducer from '../reducers/party-reducer';
export default function configureStore(initialState) {
let reducer = combineReducers({
form: formReducer,
party: partyReducer
});
let enhancements = [applyMiddleware(thunk)];
if (process.env.PROD_ENV !== 'production' && typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__) {
enhancements.push(window.__REDUX_DEVTOOLS_EXTENSION__());
}
return createStore(reducer, initialState, compose(...enhancements));
}
What I've tried
I noticed my mapDispatchToProps was written kinda strange so I fixed that, but I still get the error actions is undefined if I remove the if statement :'(
import {fetchRoles as fetchRolesAction} from '../../../actions/party-actions';
...
export const mapDispatchToProps = dispatch => ({
fetchRoles: () => dispatch(fetchRolesAction())
});
Figured it out! Was my test!
it('returns expected initState', () => {
let expected = {roles: []};
let actual = partyReducer();
expect(actual).toEqual(expected);
});
^ test above is suppose to see if the initial state is return if no state is passed in. However Actions should Always be passed in.
Fix:
it('returns expected initState', () => {
let expected = {roles: []};
let actual = partyReducer(undefined, {}); // <-- undefined state, + action
expect(actual).toEqual(expected);
});
I feel little confused, the problem is defineAvailableTouch action and state update connected to it.
Here is my code:
Actions/index.js
import {
ANIMATE_HELLO,
HANDLE_SCROLL,
IS_TOUCH_DEVICE,
SET_ABOUT_TOP,
SET_CONTACT_TOP,
SET_PORTFOLIO_TOP
} from "../Constants/ActionTypes";
export const animateHello = hello => ({
type: ANIMATE_HELLO,
payload: hello
});
export const handleScroll = scrollDelta => ({
type: HANDLE_SCROLL,
payload: scrollDelta
});
export const defineTouchAvailable = isTouchDevice => ({
type: IS_TOUCH_DEVICE,
payload: isTouchDevice
});
export const setAboutTop = aboutTop => ({
type: SET_ABOUT_TOP,
payload: aboutTop
});
export const setContactTop = contactTop => ({
type: SET_CONTACT_TOP,
payload: contactTop
});
export const setPortfolioTop = portfolioTop => ({
type: SET_PORTFOLIO_TOP,
payload: portfolioTop
});
Reducers/index.js
import {
IS_TOUCH_DEVICE,
} from "../Constants/ActionTypes";
import { initialState } from "../Constants/InitialState/InitialState";
export const rootReducer = (state = initialState, action) => {
switch(action.type) {
case ANIMATE_HELLO:
return {
...state,
hello: action.payload
};
case HANDLE_SCROLL:
return {
...state,
scrollState: action.payload
};
case IS_TOUCH_DEVICE:
console.log(action.payload); //!!!!!! THIS PRINTS EXPECTED VALUE !!!!!!!!!!
return {
...state,
isTouchDevice: action.payload
};
case SET_ABOUT_TOP:
return {
...state,
aboutTop: action.payload
};
case SET_CONTACT_TOP:
return {
...state,
contactTop: action.payload
};
case SET_PORTFOLIO_TOP:
return {
...state,
portfolioTop: action.payload
};
default:
return state
}
};
InitialState.js
export const initialState = {
scrollState: 0,
hello: 'H',
aboutTop: 0,
portfolioTop: 0,
contactTop: 0,
isTouchDevice: true
};
App.js
import React, { Component } from 'react';
import { connect } from "react-redux";
import About from "./Containers/About";
import Contact from "./Containers/Contact";
import Page from "./Containers/Page";
import Projects from "./Containers/Projects";
import {
defineTouchAvailable,
handleScroll
} from "./Actions";
window.onbeforeunload = () => {
handleScroll(0);
document.documentElement.scrollTop = 0;
};
const mapStateToProps = state => {
return {
isTouchDevice: state.isTouchDevice
}
};
const dispatchStateToProps = dispatch => {
return {
defineTouchAvailable: isTouchDevice =>
dispatch(defineTouchAvailable(isTouchDevice)),
handleScroll: scrollState => dispatch(handleScroll(scrollState))
}
};
class App extends Component {
componentDidMount() {
try {
document.createEvent('touchevent');
this.props.defineTouchAvailable(true);
} catch(e) {
this.props.defineTouchAvailable(false);
}
console.log(this.props.isTouchDevice); //!!!!!!!!!!!!!!! THIS ALWAYS PRINTS VALUE FROM initialState !!!!!!!!!!!!!!
if(this.props.isTouchDevice) {
document.documentElement.scroll(0, 1);
}
document.addEventListener('scroll', () => {
if (document.documentElement.scrollTop === 0) {
this.props.handleScroll(0);
}
});
}
render() {
return (
<div>
<Page/>
<Projects/>
<About/>
<Contact/>
</div>
);
}
}
export default connect(mapStateToProps, dispatchStateToProps)(App);
I really can't figure out whats wrong here.
As I commented
reducer console.log prints correct value that is expected to be assigned to my state (isTouchDevice field), but
after assigning it in dispatch action nothing changes - it is always value from initialState.
Can someone please explain it to me? Do I change my redux state uncorrectly? Then why other actions work as they're expected to?
The updated value of isTouchDevice will be available in componentDidUpdate, render or componentWillReceiveProps, not in componentDidMount.
componentDidMount will only be called one time when your component is mounted.
Note: componentWillReceiveProps is deprecated, better to not use it.
I have such action:
import { GET, POST, PUT, REMOVE } from "../../Utils/Http";
export const FETCH_ARTICLES = "FETCH_ARTICLES";
export const FETCH_ARTICLES_SUCCESS = "FETCH_ARTICLES_SUCCESS";
export const FETCH_ARTICLES_FAILURE = "FETCH_ARTICLES_FAILURE";
export const RESET_ARTICLES = "RESET_ARTICLES";
export function fetchArticles() {
const request = GET("/articles");
return {
type: FETCH_ARTICLES,
payload: request
};
}
export function fetchArticlesSuccess(articles) {
return {
type: FETCH_ARTICLES_SUCCESS,
payload: articles
};
}
export function fetchArticlesFailure(error) {
return {
type: FETCH_ARTICLES_FAILURE,
payload: error
};
}
and reducer:
import {
FETCH_ARTICLES,
FETCH_ARTICLES_SUCCESS,
FETCH_ARTICLES_FAILURE,
RESET_ARTICLES
} from "../Actions/Article";
const INITIAL_STATE = {
articlesList: {
articles: { data: [], total: 0 },
error: null,
loading: false
},
newTractor: { article: null, error: null, loading: false },
activeTractor: { article: null, error: null, loading: false },
deletedTractor: { article: null, error: null, loading: false }
};
const reducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case FETCH_ARTICLES:
return {
...state,
articleList: { articles: {}, error: null, loading: true }
};
case FETCH_ARTICLES_SUCCESS:
return {
...state,
articleList: { articles: action.payload, error: null, loading: false }
};
case FETCH_ARTICLES_FAILURE:
return {
...state,
articleList: { articles: {}, error: action.payload, loading: false }
};
case RESET_ARTICLES:
return {
...state,
articleList: { articles: {}, error: null, loading: false }
};
default:
return state;
}
};
export default reducer;
And i try it to use this way in list component:
import React, { Component } from "react";
import { connect } from "react-redux";
import { isUndefined } from "lodash";
import {
fetchArticles,
fetchArticlesSuccess,
fetchArticlesFailure
} from "../../Store/Actions/Article";
class ArticleList extends Component {
componentDidMount() {
this.props.fetchArticles();
}
render() {
return <div className="ui segment" />;
}
}
const mapDispatchToProps = dispatch => {
return {
fetchArticles: () => {
dispatch(fetchArticles()).then(response => {
!response.error
? dispatch(fetchArticlesSuccess(response.payload.data))
: dispatch(fetchArticlesFailure(response.payload.data));
});
}
};
};
export default connect(null, mapDispatchToProps)(ArticleList);
also Http.js:
import axios from "axios";
const http = axios.create({
baseURL: process.env.BASE_API_URL
});
export const GET = (url, params) => {
return new Promise((resolve, reject) => {
http({
method: "get",
url,
params
})
.then(response => {
resolve(response);
})
.catch(err => {
console.log("GET err ", err);
reject(err);
});
});
};
...
But as result I get:
TypeError: dispatch is not a function in dispatch(fetchArticles()).then(response => {
What I do wrong?
Also how can i write this part:
fetchTractors()).then(response => {
!response.error
? dispatch(fetchTractorsSuccess(response.payload.data))
: dispatch(fetchTractorsFailure(response.payload.data));
}
in component class? is it possible? (not to move it to the mapDispatchToProps block)
i took some ideas from here: https://github.com/rajaraodv/react-redux-blog/
I can see many problems here:
const mapDispatchToProps = dispatch => {
return {
fetchArticles: () => {
dispatch(fetchArticles()).then(response => {
!response.error
? dispatch(fetchArticlesSuccess(response.payload.data))
: dispatch(fetchArticlesFailure(response.payload.data));
});
}
};
};
dispatch is a synchronous thing by default unless you have configured some middleware such as redux-thunk to handle functions. dispatch takes native object as an argument in normal scenario.
dispatch does not return a promise. So then can not be used,
connect takes first arguments as mapStateToProps and second argument as mapDispatchtoProps. There is also third argument which is not generally used. So I will not mention it for now.
4.you need to pass the actions creators through mapDispatchToProps like this:
import { bindActionCreators } from "redux"
const mapDispatchToProps = dispatch => bindActionCreators({
fetchArticles,
fetchArticlesSuccess,
fetchArticlesFailure,
}, dispatch)
The probles is here:
export default connect(mapDispatchToProps)(ArticleList);
First parameter should be mapStateToProps. But you actually can pass null:
export default connect(null, mapDispatchToProps)(ArticleList);
If someone encountered this problem while using ts + redux, the IDE prompted you that there is no then method, you can refer to this link
I'm developing my first React Native app and it is the first time I'm using redux and redux saga. So I've built a Flatlist to have infinite scroll with a API endpoint tha returns posts (10 per page). But I don't know how to use the reducers to return the posts, control the loading indicator and keep track of the page number in the store, using redux saga.
My code is the following:
Home.js
this.state = {
page: 1,
totalPages: 10,
loading: false,
}
componentDidMount() {
this.loadMorePosts();
}
loadMorePosts = () => {
this.setState(() => { loading: true });
this.setState(() => { page: this.state.page++ });
this.props.loadPosts(this.state.page);
}
<AnimatedFlatList
...
onEndReached={this.loadMorePosts}
onEndReachedThreshold={0.2}
/>
const mapStateToProps = state => ({
posts: state.posts,
});
Posts Action
export function loadPosts(page){
return {
type: Types.FETCH_POSTS,
payload: { page }
};
}
Posts saga
export function* fetchPosts(action) {
const response = yield call(api.get, `/posts/${action.payload.page}`);
yield put({ type: Types.LOAD_POSTS, payload: { posts: response.data } });
}
Posts Reducer
export default function posts(state = initialState, action) {
switch(action.type) {
case Types.LOAD_POSTS:
return [ ...state, ...action.payload.posts ];
default:
return state;
}
}
With this I can fetch the posts and load into the Flatlist, but if I change screens I lose track of the actual page number, that will be set to 0 again in the Home.js constructor. And there is no visual feedback since the loading state is not defined with the mapStateToProps function...
Can anyone help me solve this problem?
Expanding on a comment: (Not tested code but principle is there).
Saga
export function* fetchPosts(action) {
try {
yield put({ type: Types.LOAD_POSTS_START, payload: { page: action.payload.page } });
const response = yield call(api.get, `/posts/${action.payload.page}`);
yield put({ type: Types.LOAD_POSTS, payload: { posts: response.data } });
}
catch {
//perhaps roll back page count?
yield put({ type: Types.LOAD_POSTS_END, payload: { } });
}
}
Reducer
const initialState = {
isLoading: false,
currentPage: 0,
posts: []
}
export default function posts(state = initialState, action) {
switch(action.type) {
case Types.LOAD_POSTS_START:
return {
...state,
currentPage: action.payload.page,
isLoading: true
};
case Types.LOAD_POSTS_END:
return {
...state,
isLoading: false
};
case Types.LOAD_POSTS:
return {
...state,
isLoading: false,
posts: [ ...state.posts, ...action.payload.posts ]
};
default:
return state;
}
}
Then in your component connect to this state rather than have it stored in the components state object
Make a saga/task that just does a fetch and returns a promise like this:
const fetchAction = (input, init) => {
let resolve;
const promise = new Promise(resolveArg => resolve = resolveArg)
return { type:'FETCH_ACTION', input, init, promise, resolve }
}
function fetchActionWorker({ input, init, resolve}) {
const res = yield call(fetch, input, init);
resolve(res);
}
function* fetchActionWatcher() {
yield takeEvery('FETCH_ACTION', fetchWorker);
}
Then use it like this:
class List extends Component {
render() {
return <Button title="fetch" onPress={this.doFetch} />
}
doFetch = async () => {
const res = await dispatch(fetchAction('url', { method:'GET' })).promise;
}
}
Calling the fetch action gives you a promise right away.
I am new to redux and I am having a hard time understanding how to connect the payload of my API call to my state.
Right now my action.js file looks like this:
import ApiService from '../../services/ApiService';
import { reset } from 'redux-form';
//actions
export const getStock = () => {
return {
type: 'GET_STOCK'
}
}
export const getStockPending = () => {
return {
type: 'GET_STOCK_PENDING'
}
}
export const getStockFulfilled = (stock) => {
return {
type: 'GET_STOCK_FULFILLED',
payload: stock
}
}
export const getStockRejected = () => {
return {
type: 'GET_STOCK_REJECTED'
}
}
// async function calls
export function fetchStocksWithRedux() {
const action_type = "GET_STOCK";
const stock = 'AAPL';
return (dispatch) => {
dispatch({type: `${action_type}_PENDING`});
return ApiService.get(`/search?query=${stock}`)
.then(([response, json]) =>{
if(response.status === 200){
dispatch(getStockFulfilled(json))
}
else{
dispatch(getStockRejected())
}
})
}
}
and my reducer.js file looks like this:
const initialState = {
inProgress: false,
stock: {},
stocks: ['NKE', 'AMZN', 'AAPL'],
error: {}
}
export default (state = initialState, action) => {
switch(action.type) {
case 'GET_STOCK_PENDING':
return {
...state,
inProgress: true,
error: false
}
case 'GET_STOCK_FULFILLED':
return {
...state,
stock: action.payload,
inProgress: false
}
case 'GET_STOCK_REJECTED':
return {
...state,
inProgress: false,
error: action.error
}
default:
return state;
}
}
When I go to call my method fetchStocksWithRedux in my component, the network tab in my dev tools shows a 200 status and the response I'm expecting, but the reducer dispatches the 'GET_STOCK_REJECTED' action, but the error hash is empty. What do you think is going wrong?
Here is my component, for reference:
import React, { Component } from 'react';
import { fetchStocksWithRedux } from '../../redux/modules/Stock/actions';
import { connect } from 'react-redux';
class Dashboard extends Component {
componentDidMount() {
this.props.fetchStocksWithRedux()
}
render() {
return (
<div className="uk-position-center">
</div>
)
}
}
export default connect(
state => ({
stocks: state.stocks,
stock: state.stock
})
, { fetchStocksWithRedux }
)(Dashboard);
Thanks. Any advice or guidance would be greatly appreciated!