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

Implementing infinite scroll with React/Redux and react-waypoint issue

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

这里用简单的话来说它是如何工作的:

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

2) 我实现了 react-waypoint,所以当你滚动到这些照片的底部时,它会触发一个方法,该方法会调度另一个获取更多照片的动作,并将它们 "appends" 到照片数组,然后...

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

这当然不是我想要的。

没有 Redux 的 react-waypoint 的简单例子是这样的:

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

并且有效。没有完全重新渲染组件。滚动位置保持不变,新项目出现在航点下方。

所以问题是——我遇到的问题是 Redux 状态管理问题还是我没有正确实施我的 redux reducers/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)));
};

我还包括渲染照片的主要组件,因为我认为这可能与我 "connect" 这个组件到 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. 初始状态496张,获取后996张 到达航点后的第一张照片
  4. here is action.data

所以我想说的是,照片已获取并附加,但它仍然会触发组件的整个重新渲染...

编辑2

尝试注释掉整个 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);
    }
  });
}

而不是

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-y 值,即使它是一个空数组。您可以将其更改为

state.photos.length ? .. : ..

如果没有真正看到你如何在 reducersactions 中更新 photos 很难说,但我怀疑这是 Redux 管理状态的问题。

当您从 ajax 请求中获取新照片时,传入的新照片应附加到 store.

中照片数组的末尾

例如,如果当前 photos: [<Photo Z>, <Photo F>, ...] 在 Redux store 中,而 action 中的新照片是 photos: [<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,
        ],
      };
...

我想我明白问题了。

在您的组件中检查

{!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。这告诉 React 删除整个照片组件,然后一旦它设置为 false 再次 React 将显示新照片。

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