3

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.

  1. Here is my initial state after getting photos
  2. Here is the changed state after getting new portion of photos
  3. There were 496 photos in the initial state, and 996 after getting additional photos for the first time after reaching waypoint
  4. here is action.data

So all I want to say that the photos are fetched and appended but it triggers whole re-render of the component still...

2
  • Can you post your code (especially the reducer and the component) so we will be able to see what went wrong there? Commented Mar 24, 2018 at 7:32
  • Just added. Thanks! Commented Mar 24, 2018 at 7:56

2 Answers 2

4

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you very much! That solved the problem! Oooh, I feel relieved.
@dejavu2012 glad I could help mate
1

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,
        ],
      };
...

3 Comments

Thanks. I posted my reducers code. I think Im doing like you say, but it still works the same =( I added the ...state and it still works the same. I would appreciate if you take a look. Thanks!
Yes. I answered you above because I needed to add pics. Its at the bottom of my post. Thanks!
@dejavu2012 try what I added on the top of my answer

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.