Reputation: 75
Im struggling to achieve infinite scroll with my test React/Redux application.
Here how it works in simple words:
1) On componentDidMount I dispatch an action which sets the Redux state after getting 100 photos from the API. So I got photos array in Redux state.
2) I implemented react-waypoint, so when you scroll to the bottom of those photos it fires a method which dispatches another action that get more photos and "appends" them to the photos array and...
as I understand - the state changed, so redux is firing the setState and the component redraws completely, so I need to start scrolling again but its 200 photos now. When I reach waypoint again everything happens again, component fully rerenders and I need to scroll from top through 300 photos now.
This is not how I wanted it to work of course.
The simple example on react-waypoint without Redux works like this:
1) you fetch first photos and set the components initial state 2) after you scroll to the waypoint it fires a method which makes another request to the api, constructs new photos array(appending newly fetched photos) and (!) call setState with the new photos array.
And it works. No full re-renders of the component. Scroll position stays the same, and the new items appear below waypoint.
So the question is — is the problem I experience the problem with Redux state management or am I implementing my redux reducers/actions not correctly or...???
Why is setting component state in React Waypoint Infinite Scroll example (no Redux) works the way I want (no redrawing the whole component)?
I appreciate any help! Thank you!
The reducers
import { combineReducers } from 'redux';
const data = (state = {}, action) => {
if (action.type === 'PHOTOS_FETCH_DATA_SUCCESS') {
const photos = state.photos ?
[...state.photos, ...action.data.photo] :
action.data.photo;
return {
photos,
numPages: action.data.pages,
loadedAt: (new Date()).toISOString(),
};
}
return state;
};
const photosHasErrored = (state = false, action) => {
switch (action.type) {
case 'PHOTOS_HAS_ERRORED':
return action.hasErrored;
default:
return state;
}
};
const photosIsLoading = (state = false, action) => {
switch (action.type) {
case 'PHOTOS_IS_LOADING':
return action.isLoading;
default:
return state;
}
};
const queryOptionsIntitial = {
taste: 0,
page: 1,
sortBy: 'interestingness-asc',
};
const queryOptions = (state = queryOptionsIntitial, action) => {
switch (action.type) {
case 'SET_TASTE':
return Object.assign({}, state, {
taste: action.taste,
});
case 'SET_SORTBY':
return Object.assign({}, state, {
sortBy: action.sortBy,
});
case 'SET_QUERY_OPTIONS':
return Object.assign({}, state, {
taste: action.taste,
page: action.page,
sortBy: action.sortBy,
});
default:
return state;
}
};
const reducers = combineReducers({
data,
photosHasErrored,
photosIsLoading,
queryOptions,
});
export default reducers;
Action creators
import tastes from '../tastes';
// Action creators
export const photosHasErrored = bool => ({
type: 'PHOTOS_HAS_ERRORED',
hasErrored: bool,
});
export const photosIsLoading = bool => ({
type: 'PHOTOS_IS_LOADING',
isLoading: bool,
});
export const photosFetchDataSuccess = data => ({
type: 'PHOTOS_FETCH_DATA_SUCCESS',
data,
});
export const setQueryOptions = (taste = 0, page, sortBy = 'interestingness-asc') => ({
type: 'SET_QUERY_OPTIONS',
taste,
page,
sortBy,
});
export const photosFetchData = (taste = 0, page = 1, sort = 'interestingness-asc', num = 500) => (dispatch) => {
dispatch(photosIsLoading(true));
dispatch(setQueryOptions(taste, page, sort));
const apiKey = '091af22a3063bac9bfd2e61147692ecd';
const url = `https://api.flickr.com/services/rest/?api_key=${apiKey}&method=flickr.photos.search&format=json&nojsoncallback=1&safe_search=1&content_type=1&per_page=${num}&page=${page}&sort=${sort}&text=${tastes[taste].keywords}`;
// console.log(url);
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
dispatch(photosIsLoading(false));
return response;
})
.then(response => response.json())
.then((data) => {
// console.log('vvvvv', data.photos);
dispatch(photosFetchDataSuccess(data.photos));
})
.catch(() => dispatch(photosHasErrored(true)));
};
I also include my main component that renders the photos because I think maybe it's somehow connected with the fact that i "connect" this component to Redux store...
import React from 'react';
import injectSheet from 'react-jss';
import { connect } from 'react-redux';
import Waypoint from 'react-waypoint';
import Photo from '../Photo';
import { photosFetchData } from '../../actions';
import styles from './styles';
class Page extends React.Component {
loadMore = () => {
const { options, fetchData } = this.props;
fetchData(options.taste, options.page + 1, options.sortBy);
}
render() {
const { classes, isLoading, isErrored, data } = this.props;
const taste = 0;
const uniqueUsers = [];
const photos = [];
if (data.photos && data.photos.length > 0) {
data.photos.forEach((photo) => {
if (uniqueUsers.indexOf(photo.owner) === -1) {
uniqueUsers.push(photo.owner);
photos.push(photo);
}
});
}
return (
<div className={classes.wrap}>
<main className={classes.page}>
{!isLoading && !isErrored && photos.length > 0 &&
photos.map(photo =>
(<Photo
key={photo.id}
taste={taste}
id={photo.id}
farm={photo.farm}
secret={photo.secret}
server={photo.server}
owner={photo.owner}
/>))
}
</main>
{!isLoading && !isErrored && photos.length > 0 && <div className={classes.wp}><Waypoint onEnter={() => this.loadMore()} /></div>}
{!isLoading && !isErrored && photos.length > 0 && <div className={classes.wp}>Loading...</div>}
</div>
);
}
}
const mapStateToProps = state => ({
data: state.data,
options: state.queryOptions,
hasErrored: state.photosHasErrored,
isLoading: state.photosIsLoading,
});
const mapDispatchToProps = dispatch => ({
fetchData: (taste, page, sort) => dispatch(photosFetchData(taste, page, sort)),
});
const withStore = connect(mapStateToProps, mapDispatchToProps)(Page);
export default injectSheet(styles)(withStore);
Answer to Eric Na
state.photos is an object and I just check if its present in the state. sorry, in my example I just tried to simplify things.
action.data.photo is an array for sure. Api names it so and I didn't think about renaming it.
I supplied some pics from react dev tools.
So all I want to say that the photos are fetched and appended but it triggers whole re-render of the component still...
Upvotes: 3
Views: 4614
Reputation: 2813
I think I see the problem.
In your component you check for
{!isLoading && !isErrored && photos.length > 0 &&
photos.map(photo =>
(<Photo
key={photo.id}
taste={taste}
id={photo.id}
farm={photo.farm}
secret={photo.secret}
server={photo.server}
owner={photo.owner}
/>))
}
once you make another api request, in your action creator you set isLoading
to true
. this tells react to remove the whole photos component and then once it's set to false
again react will show the new photos.
you need to add a loader at the bottom and not to remove the whole photos component once fetching and then render it again.
Upvotes: 4
Reputation: 2705
EDIT2
Try commenting out the whole uniqueUsers
part (let's worry about the uniqueness of the users later)
const photos = [];
if (data.photos && data.photos.length > 0) {
data.photos.forEach((photo) => {
if (uniqueUsers.indexOf(photo.owner) === -1) {
uniqueUsers.push(photo.owner);
photos.push(photo);
}
});
}
and instead of
photos.map(photo =>
(<Photo ..
try directly mapping data.photos
?
data.photos.map(photo =>
(<Photo ..
EDIT
...action.data.photo] :
action.data.photo;
can you make sure it's action.data.photo
, not action.data.photos
, or even just action.data
? Can you try logging the data to the console?
Also,
state.photos ? .. : ..
Here, state.photos
will always evaluate to true
-y value, even if it's an empty array. You can change it to
state.photos.length ? .. : ..
It's hard to tell without actually seeing how you update photos
in reducers
and actions
, but I doubt that it's the problem with how Redux manages state.
When you get new photos from ajax request, the new photos coming in should be appended to the end of the photos array in the store
.
For example, if currently photos: [<Photo Z>, <Photo F>, ...]
in Redux store
, and the new photos in action
is photos: [<Photo D>, <Photo Q>, ...]
, the photos
in store
should be updated like this:
export default function myReducer(state = initialState, action) {
switch (action.type) {
case types.RECEIVE_PHOTOS:
return {
...state,
photos: [
...state.photos,
...action.photos,
],
};
...
Upvotes: 1