简体   繁体   English

使用减速机state里面使用效果

[英]Using reducer state inside useEffect

Hello All I have a question about our favorite Hooks API!大家好,我有一个关于我们最喜欢的 Hooks API 的问题!

What am I trying to do?我想做什么?

I am trying to fetch photos from some remote system.我正在尝试从某个远程系统获取照片。 I store the blob urls for these photos in my reducer state keyed by an id.我将这些照片的 blob url 存储在我的减速器 state 中,由 id 键入。

I have a helper function wrapped in the memoized version returned by the useCallback hook.我有一个助手 function 包裹在useCallback钩子返回的记忆版本中。 This function is called in the useEffect I have defined.这个 function 在我定义的useEffect中被调用。

The Problem ⚠️问题⚠️

My callback aka the helper function depends on part of the reducer state.我的回调又名助手 function 取决于减速器 state 的一部分。 Which is updated every time a photo is fetched.每次获取照片时都会更新。 This causes the component to run the effect in useEffect again and thus causing an infinite loop.这会导致组件再次在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]);

  ...
  ...
}

What have I tried?我尝试了什么?

I found an alternative way to fetch photos aka by dispatching an action and using a worker saga to do all the fetching.我找到了另一种获取照片的方法,也就是通过调度一个动作并使用一个工人传奇来完成所有的获取。 This removes all the need for the helper in the component and thus no useCallback and thus no re-renders.这消除了组件中对帮助程序的所有需求,因此没有useCallback ,因此也没有重新渲染。 The useEffect then only depends on the dispatch which is fine.然后 useEffect 仅取决于很好的dispatch

Question?问题?

I am struggling with the mental modal of using the hooks API.我正在为使用钩子 API 的心理模式而苦苦挣扎。 I see the obvious problem, but I am not sure how could this be done without using redux middlewares like thunks and sagas.我看到了明显的问题,但我不确定如果不使用 redux 中间件(如 thunk 和 sagas)如何做到这一点。

Edit:编辑:

reducer function:减速机function:

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

You could include the photoIdsToFetch calculation logic into your selector function, to reduce the number of renders caused by state change.您可以将photoIdsToFetch计算逻辑包含到您的选择器 function 中,以减少由 state 更改引起的渲染次数。

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

However the selector function isn't memoized, it returns a new array object every time, thus object equality will not work here.然而选择器 function 没有被记忆,它每次都返回一个新数组 object,因此 object 相等在这里不起作用。 You will need to provide an isEqual method as a second parameter (that will compare two arrays for value equality) so that the selector will return the same object when the ids are the same.您需要提供一个 isEqual 方法作为第二个参数(它将比较两个 arrays 的值是否相等),以便选择器在 id 相同时返回相同的 object。 You could write your own or deep-equals library for example:您可以编写自己的或deep-equals库,例如:

import equal from 'deep-equal';

fetchFormPhotos will depend only on [photoIdsToFetch, dispatch] this way. fetchFormPhotos将仅依赖于[photoIdsToFetch, dispatch]这种方式。

I'm not sure about how your reducer functions mutate the state, so this may require some fine tuning.我不确定你的减速器功能如何改变 state,所以这可能需要一些微调。 The idea is: select only the state from store that you depend on, that way other parts of the store will not cause re-renders.这个想法是: select 仅来自您所依赖的商店的 state,这样商店的其他部分就不会导致重新渲染。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM