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.
Related
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 ;)
I have the following Reducer:
const initialState = {}
const dishReducer = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_DISHES':
return (action.dishes)
case 'LOAD_DISHES_ERROR':
console.log("load dishes error")
return state
case 'LOAD_DISHES_SUCCESS':
console.log("load dishes success")
return state
default:
return state;
}
};
export default dishReducer;
And the following action(s):
import {database} from '../../config/fbConfig'
export const startLoadingDishes = (dishes) => {
return (dispatch) =>{
return database.ref('products-dishes').once('value').then((snapshot) => {
let dishes = {}
snapshot.forEach((childSnapshot) => {
let parentkey = childSnapshot.key
let dishArray = [];
childSnapshot.forEach((dish) =>{
dishArray.push(dish.val())
});
dishes[childSnapshot.key] = dishArray;
})
dispatch(loadDishes(dishes))
}).then(() => {
dispatch({ type: 'LOAD_DISHES_SUCCESS' });
}).catch(err => {
dispatch({ type: 'LOAD_DISHES_ERROR' }, err);
});
}
}
export const loadDishes = (dishes) => {
return {
type: 'LOAD_DISHES',
dishes
}
}
The 'startLoadingDishes' action is called inside the componentDidLoad() of a certain Component. However, I want to alter the initial state of my dishReducer so that it includes additional information, as follows:
const initialState = {
value : {},
loaded: false,
loading: false,
error: false
}
So now 'action.dishes' returned by reducer [in 'LOAD_DISHES' case] should be put inside the 'value' part of the state, instead of it being the whole state. Also, the 'loaded' part of the state should be set to true if dishes have already been loaded earlier, and so on. I understand this is fairly simple but as I am new to React+Redux, I don't know how to alter the Action/Reducer codes properly (while keeping state immutability). Any help is appreciated.
I originally asked the question, here is how I solved it (not sure if this is the 'best' way though):
New reducer file:
const initialState = {
value : {},
loaded: false,
loading: false,
error: false
}
const dishReducer = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_DISHES':
return {
value: action.dishes,
loading: !state.loading,
loaded: false, //this might need to be set to true
error: false
}
case 'LOAD_DISHES_ERROR':
console.log("load dishes error")
return {
...state, //or: state.value, as the other parts of state are being overwritten below
loaded: false,
loading: false,
error: true
}
case 'LOAD_DISHES_SUCCESS':
console.log("load dishes success")
return {
...state, //better: state.value
loaded: true,
loading: false,
error: false
}
default:
return state;
}
};
export default dishReducer;
No change in actions file.
Now, inside the 'Main' component, I was originally accessing the state as such:
class Main extends Component {
componentDidMount() {
this.props.startLoadingDishes();
}
render() {
return (
//some code
)
}
}
const mapStateToProps = (state) => {
return {
dishes: state.dishes //to access dishes: dishes.value
}
}
export default connect(mapStateToProps, actions)(Main)
The Main component code also stayed the same, with the difference that now I use 'dishes.value' instead of just 'dishes' to access the value of dishes from the state (and dishes.loaded for loaded, and so on). And now the action caller inside componentDidMount is as follows:
componentDidMount() {
if(!this.props.dishes.loaded){
this.props.startLoadingDishes();
console.log("loading dishes from database")
}
}
I am trying to develop a simple image list component with react-redux stack.
This are my actions, reducers, saga and component root definitions -
// Actions
export const getImageListData = () => ({
type: IMAGE_LIST_GET_DATA
});
export const getImageListDataSuccess = (data) => {
console.log("ACTION::SUCCESS", data);
return ({
type: IMAGE_LIST_GET_DATA_SUCCESS,
payload: data
});
};
// Reducers
export default (state = INIT_STATE, action) => {
console.log("REDUCER::", state, action);
switch (action.type) {
case IMAGE_LIST_GET_DATA: return { ...state, isLoading: true };
case IMAGE_LIST_GET_DATA_SUCCESS: return { ...state, items: action.payload.data, isLoading: false };
default: return { ...state };
}
}
// Sagas
import imagesData from "Data/images.json";
function* loadImages() {
try {
const response = yield call(loadImagesAsync);
console.log("SAGA:: ", response);
yield put(getImageListDataSuccess(response));
} catch (error) {
console.log(error);
}
}
const loadImagesAsync = async () => {
const contacts = imagesData;
return await new Promise((success, fail) => {
setTimeout(() => {
success(contacts);
}, 2000);
}).then(response => response).catch(error => error);
};
export function* watchGetImages() {
console.log("ACTION::INIT", IMAGE_LIST_GET_DATA);
yield takeEvery(IMAGE_LIST_GET_DATA, loadImages);
}
export default function* rootSaga() {
yield all([
fork(watchGetImages)
]);
}
Now in the component I am calling - getImageListData action
and with this mapStateToProps and connect provider -
const mapStateToProps = ({ ImageList }) => {
const {items} = ImageList;
return {items};
};
export default connect(
mapStateToProps,
{
getImageListData
}
)(ImageListLayout);
I am mapping the image list response to the component props.
My component definition is as follows -
class ImageListLayout extends Component {
constructor(props) {
super(props);
this.state = {
displayMode: "imagelist",
pageSizes: [8, 12, 24],
selectedPageSize: 8,
categories: [
{label:'Cakes',value:'Cakes',key:0},
{label:'Cupcakes',value:'Cupcakes',key:1},
{label:'Desserts',value:'Desserts',key:2},
],
orderOptions:[
{column: "title",label: "Product Name"},
{column: "category",label: "Category"},
{column: "status",label: "Status"}
],
selectedOrderOption: {column: "title",label: "Product Name"},
dropdownSplitOpen: false,
modalOpen: false,
currentPage: 1,
items: [],
totalItemCount: 0,
totalPage: 1,
search: "",
selectedItems: [],
lastChecked: null,
displayOptionsIsOpen: false,
isLoading:false
};
}
componentDidMount() {
this.dataListRender();
}
dataListRender() {
this.props.getImageListData();
}
render() {
...
}
}
Now in my component I am able to correctly access this.props.items obtained from reducer with action IMAGE_LIST_GET_DATA_SUCCESS, but I also want to update some of the state variables like isLoading, currentPage, totalItemCount, totalPage and since these belong to this component itself and not their parents I do not want to map them to the props but want to update the state of the component and trigger a re-render.
Can someone please tell me what should I be doing to fix this or am i missing anything else here ?
In your current setup I see no reason for you to have isLoading, etc. in the state. You should just map it to props:
const mapStateToProps = ({ ImageList, isLoading }) => {
const {items} = ImageList;
return {items, isLoading};
};
I don't get why you say "and since these belong to this component itself and not their parents I do not want to map them to the props but want to update the state of the component and trigger a re-render." what do parents have to do with anything here?
I'm fairly new to redux & thunk, and have been following tutorials to try and understand, and am managing to work it into my app ok. One thing i'm not understanding, is how i can get several state objects on the root level into one nested object. For example, right now my state looks like:
{
timeline: [Array] // My timeline data in an array of objects
timelineHasErrored: false,
timelineIsLoading: false
}
But what I really want is:
{
timeline : {
data: [Array] // My timeline data in an array of objects
hasErrored: false,
isLoading: false
}
}
and i'm really not quite sure how to nest these, or what the proper way to do that is. Below is my redux code, it's pretty simple so i'll post it all.
Reducers index
import { combineReducers } from 'redux'
import { timeline, timelineHasErrored, timelineIsLoading } from './timeline'
export default combineReducers({
timeline, timelineHasErrored, timelineIsLoading
});
Timeline Reducers
import { TIMELINE_HAS_ERRORED, TIMELINE_IS_LOADING, TIMELINE_FETCH_DATA_SUCCESS } from '../constants/action-types.js'
export function timelineHasErrored(state = false, action) {
switch (action.type) {
case TIMELINE_HAS_ERRORED:
return action.hasErrored;
default:
return state;
}
}
export function timelineIsLoading(state = false, action) {
switch (action.type) {
case TIMELINE_IS_LOADING:
return action.isLoading;
default:
return state;
}
}
export function timeline(state = [], action) {
switch (action.type) {
case TIMELINE_FETCH_DATA_SUCCESS:
return action.timeline;
default:
return state;
}
}
Actions
import { TIMELINE_HAS_ERRORED, TIMELINE_IS_LOADING, TIMELINE_FETCH_DATA_SUCCESS } from '../constants/action-types.js'
import api from '../services/api'
export function timelineHasErrored(bool) {
return {
type : TIMELINE_HAS_ERRORED,
hasErrored : bool
}
}
export function timelineIsLoading(bool) {
return {
type : TIMELINE_IS_LOADING,
isLoading : bool
}
}
export function timelineFetchDataSuccess(timeline) {
return {
type : TIMELINE_FETCH_DATA_SUCCESS,
timeline
}
}
export function timelineFetchData() {
return dispatch => {
dispatch( timelineIsLoading(true) )
api.getTracks().then(
res => {
dispatch( timelineIsLoading(false) )
dispatch( timelineFetchDataSuccess(res.body) )
},
err => {
dispatch( timelineIsLoading(false) )
dispatch( timelineHasErrored(true) )
}
)
}
}
And then in my react component I format the object like how i want it... but i think it would be better to have it nested in the actual state so i'm not creating extra work for myself if things change
// Redux State
const mapStateToProps = (state) => {
const obj = {
timeline : {
data : state.timeline,
hasErrored: state.tracksHasErrored,
isLoading: state.tracksIsLoading
}
}
return obj
}
// Redux Dispatch
const mapDispatchToProps = (dispatch) => {
return {
fetchData: () => dispatch( timelineFetchData() )
}
}
If anybody has any tips or corrections for me bring em on, i'm trying to get a solid grasp on redux, thanks!
Your timeline reducer is pretty small, so you could have it as a single reducer as follows:
const initialState = {
data: [],
hasErrored: false,
isLoading: false
};
export function timeline(state = initialState, action) {
switch (action.type) {
case TIMELINE_HAS_ERRORED:
return {
...state,
hasErrored: action.hasErrored
};
case TIMELINE_IS_LOADING:
return {
...state,
isLoading: action.isLoading
};
case TIMELINE_FETCH_DATA_SUCCESS:
return {
...state,
data: action.timeline
};
default:
return state;
}
}
Then you wouldn't need to call combineReducers(), unless you had other reducers.
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!