javascript - 在 useEffect 中使用 reducer 状态

标签 javascript reactjs redux react-redux

大家好👋🏻我有一个关于我们最喜欢的 Hooks API 的问题!

我想做什么?

我正在尝试从某个远程系统获取照片。我将这些照片的 blob url 存储在由 id 键控的 reducer 状态中。

我有一个辅助函数,包含在 useCallback Hook 返回的内存版本中。这个函数在我定义的useEffect中被调用。

问题⚠️

我的回调又名辅助函数取决于 reducer 状态的一部分。每次获取照片时都会更新。这会导致组件再次运行 useEffect 中的效果,从而导致无限循环。

component renders --> useEffect runs ---> `fetchPhotos` runs --> after 1st photo, reducer state is updated --> component updates because `useSelector`'s value changes ---> runs `fetchPhotos` again ---> infinite
const FormViewerContainer = (props) => {
  const { completedForm, classes } = props;

  const [error, setError] = useState(null);

  const dispatch = useDispatch();
  const photosState = useSelector(state => state.root.photos);

  // helper function which fetches photos and updates the reducer state by dispatching actions
  const fetchFormPhotos = React.useCallback(async () => {
    try {
      if (!completedForm) return;
      const { photos: reducerPhotos, loadingPhotoIds } = photosState;
      const { photos: completedFormPhotos } = completedForm;
      const photoIds = Object.keys(completedFormPhotos || {});
      
      // only fetch photos which aren't in reducer state yet
      const photoIdsToFetch = photoIds.filter((pId) => {
        const photo = reducerPhotos[pId] || {};
        return !loadingPhotoIds.includes(pId) && !photo.blobUrl;
      });

      dispatch({
        type: SET_LOADING_PHOTO_IDS,
        payload: { photoIds: photoIdsToFetch } });

      if (photoIdsToFetch.length <= 0) {
        return;
      }

      photoIdsToFetch.forEach(async (photoId) => {
        if (loadingPhotoIds.includes(photoIds)) return;

        dispatch(fetchCompletedFormPhoto({ photoId }));
        const thumbnailSize = {
          width: 300,
          height: 300,
        };

        const response = await fetchCompletedFormImages(
          cformid,
          fileId,
          thumbnailSize,
        )

        if (response.status !== 200) {
          dispatch(fetchCompletedFormPhotoRollback({ photoId }));
          return;
        }
    
        const blob = await response.blob();
        const blobUrl = URL.createObjectURL(blob);

        dispatch(fetchCompletedFormPhotoSuccess({
          photoId,
          blobUrl,
        }));
      });
    } catch (err) {
      setError('Error fetching photos. Please try again.');
    }
  }, [completedForm, dispatch, photosState]);

  // call the fetch form photos function
  useEffect(() => {
    fetchFormPhotos();
  }, [fetchFormPhotos]);

  ...
  ...
}

我尝试了什么?

我找到了另一种获取照片的方法,即分派(dispatch)一个操作并使用工作人员传奇来完成所有获取操作。这消除了组件中对助手的所有需求,因此没有 useCallback ,因此也不需要重新渲染。那么 useEffect 只依赖于 dispatch ,这很好。

问题?

我正在努力应对使用 hooks API 的心理模式。我看到了明显的问题,但我不确定如果不使用像 thunk 和 sagas 这样的 redux 中间件,如何做到这一点。

编辑:

reducer 功能:

export const initialState = {
  photos: {},
  loadingPhotoIds: [],
};

export default function photosReducer(state = initialState, action) {
  const { type, payload } = action;
  switch (type) {
    case FETCH_COMPLETED_FORM_PHOTO: {
      return {
        ...state,
        photos: {
          ...state.photos,
          [payload.photoId]: {
            blobUrl: null,
            error: false,
          },
        },
      };
    }
    case FETCH_COMPLETED_FORM_PHOTO_SUCCESS: {
      return {
        ...state,
        photos: {
          ...state.photos,
          [payload.photoId]: {
            blobUrl: payload.blobUrl,
            error: false,
          },
        },
        loadingPhotoIds: state.loadingPhotoIds.filter(
          photoId => photoId !== payload.photoId,
        ),
      };
    }
    case FETCH_COMPLETED_FORM_PHOTO_ROLLBACK: {
      return {
        ...state,
        photos: {
          ...state.photos,
          [payload.photoId]: {
            blobUrl: null,
            error: true,
          },
        },
        loadingPhotoIds: state.loadingPhotoIds.filter(
          photoId => photoId !== payload.photoId,
        ),
      };
    }
    case SET_LOADING_PHOTO_IDS: {
      return {
        ...state,
        loadingPhotoIds: payload.photoIds || [],
      };
    }
    default:
      return state;
  }
}

最佳答案

您可以将 photoIdsToFetch 计算逻辑包含到选择器函数中,以减少状态更改导致的渲染次数。

const photoIdsToFetch = useSelector(state => {
    const { photos: reducerPhotos, loadingPhotoIds } = state.root.photos;
    const { photos: completedFormPhotos } = completedForm;
    const photoIds = Object.keys(completedFormPhotos || {});
    const photoIdsToFetch = photoIds.filter(pId => {
      const photo = reducerPhotos[pId] || {};
      return !loadingPhotoIds.includes(pId) && !photo.blobUrl;
    });
    return photoIdsToFetch;
  },
  equals
);

但是选择器函数没有被内存,它每次都会返回一个新的数组对象,因此对象相等在这里不起作用。您需要提供 isEqual 方法作为第二个参数(它将比较两个数组的值是否相等),以便当 id 相同时选择器将返回相同的对象。您可以编写自己的库或 deep-equals 库,例如:

import equal from 'deep-equal';

fetchFormPhotos 将仅依赖于 [photoIdsToFetch,dispatch] 这种方式。

我不确定你的 reducer 函数如何改变状态,所以这可能需要一些微调。这个想法是:仅从存储中选择您所依赖的状态,这样存储的其他部分就不会导致重新渲染。

关于javascript - 在 useEffect 中使用 reducer 状态,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58294789/

相关文章:

javascript - 读取文本文件然后获取内容并显示到表 yii2

javascript - 我可以在 Angular + ngrx 中使用对象扩展语法吗?

reactjs - 在应用程序初始化时从持久存储中检索 token React Native、Redux-Persist

javascript - page.evaluate - 如何写入其中的文件?

javascript - 为什么引用未声明的变量会引发引用异常,而引用未声明的属性却不会?

javascript - 针对 IE 8 及以下版本?

javascript - 将 Charts.js 与 react 一起使用

reactjs - Nginx:在同一服务器上为多个 React 应用程序提供服务

javascript - 在componentDidMount()内部的两个 `then`中执行函数

json - firebase 设置失败的第一个参数在属性中包含未定义