使用React / Redux和react-waypoint问题实现无限滚动

时间:2018-03-24 07:01:31

标签: reactjs redux infinite-scroll

我正在努力通过我的测试React / Redux应用程序实现无限滚动。

这里的工作原理用简单的词语表达:

1)在componentDidMount上,我发送一个动作,在从API获取100张照片后设置Redux状态。所以我在Redux州得到了照片阵列。

2)我实现了react-waypoint,所以当你滚动到那些照片的底部时,它会触发一个方法,该方法会调度另一个动作,从而获得更多照片并将它们“追加”到照片数组中......

据我所知 - 状态发生了变化,因此redux正在触发setState并且组件完全重绘,所以我需要再次开始滚动,但现在只需要200张照片。当我再次到达航点时,一切都会再次发生,组件完全重新渲染,我现在需要从顶部滚动到300张照片。

当然,这不是我想要的方式。

没有Redux的react-waypoint上的简单示例如下:

1)您获取第一张照片并设置组件的初始状态 2)滚动到航点后,它会触发一个方法,该方法向api发出另一个请求,构建新的照片数组(附加新获取的照片)和(!)使用新的照片数组调用setState。

它有效。没有完整的组件重新渲染。滚动位置保持不变,新项目显示在航点下方。

所以问题是 - 问题是我遇到了Redux状态管理的问题,还是我没有正确地实现我的redux reducer / actions或...... ???

为什么在React Waypoint Infinite Scroll example(没有Redux)中设置组件状态按照我想要的方式工作(不重绘整个组件)?

我感谢任何帮助!谢谢!

缩减器

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;

行动创作者

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)));
};

我还包含了渲染照片的主要组件,因为我觉得它可能与我将这个组件“连接”到Redux商店的事实联系起来......

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);

回答Eric Na

state.photos是一个对象,我只是检查它是否存在于状态中。对不起,在我的例子中,我只是想简化一些事情。

action.data.photo肯定是一个数组。 Api将其命名为我,并且我没有考虑重命名它。

我从react开发工具提供了一些照片。

  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
  5. 所以我想说的是,这些照片都是获取并附加的,但它仍会触发整个组件的重新渲染......

2 个答案:

答案 0 :(得分:3)

我想我看到了问题。

在您的组件中检查

{!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}
              />))
          }

一旦您再次发出api请求,请在您的操作创建者中将isLoading设置为true。这告诉你做出反应删除整个照片组件,然后一旦设置为false再次反应就会显示新照片。

你需要在底部添加一个加载器,而不是在取出后删除整个照片组件,然后再次渲染它。

答案 1 :(得分:1)

EDIT2

尝试评论整个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); } }); } 部分(稍后让我们担心用户的独特性)

photos.map(photo =>
  (<Photo ..

而不是

data.photos

尝试直接映射data.photos.map(photo => (<Photo ..

...action.data.photo] :
     action.data.photo;

修改

action.data.photo

你能确定它是action.data.photos,而不是action.data,甚至只是state.photos ? .. : .. 吗?您可以尝试将数据记录到控制台吗?

此外,

state.photos

此处,true将始终评估为state.photos.length ? .. : .. - y值,即使它是一个空数组。您可以将其更改为

photos

如果没有真正了解reducersactionsstore的更新方式,很难说清楚,但我怀疑Redux管理状态的方式存在问题。

当你从ajax请求中获取新照片时,新照片应该附加到photos: [<Photo Z>, <Photo F>, ...]中照片数组的末尾。

例如,如果Redux store中的当前actionphotos: [<Photo D>, <Photo Q>, ...]中的新照片为photosstore中的export default function myReducer(state = initialState, action) { switch (action.type) { case types.RECEIVE_PHOTOS: return { ...state, photos: [ ...state.photos, ...action.photos, ], }; ... 应该像这样更新:

file:///your_local_path