redux 多个组件决定同时提取对象

cu6pst1q  于 2022-11-12  发布在  其他
关注(0)|答案(1)|浏览(138)

我有一个React组件,看起来像这样:

function MyComponent(props) {
    const dispatch = useDispatch(); // Redux dispatch function
    const isLoading = useSelector(selIsLoading); // flag from our Redux app state
    const data = useSelector(selData); // data from our Redux app state

    useEffect(() => {
        if (!data && !isLoading) {
            console.log('Need to fetch!');
            dispatch(actionToFetchStuff());
        }
    }, [data, isLoading]);

    return !data ? <NoData /> : <Data data={data} ...props />;
}

问题是:当页面上有多个MyComponent的示例同时加载时,useEffect的闭包已经捕获了isLoadingdatainitial 值。这就是为什么所有组件都得出结论,它们需要同时获取对象。防止这种情况的isLoading标志稍后出现,因为dispatch(或者更确切地说,useEffect的效果)是(伪)异步的。
我可以使用saga,但我宁愿不使用,因为这是一个非常简单的组件,而传奇故事往往会使事情变得复杂。我正在寻找一种聪明的方法,使用React钩子或类似的东西,而不会使我的组件过于复杂。

wribegjk

wribegjk1#

最后我实现了一个钩子来解决这个问题。我的组件变成了:

function MyComponent(props) {
    const [data, isLoading] = useRemoteData(props, () => ({
        isLoading: selIsLoading,
        select: selData,
        shouldFetch: (_state, _props, extras) => 
            !extras.selected && !extras.isLoading,
        fetch: dispatch => dispatch(actionToFetchStuff()),
    }));
    return !data ? <NoData /> : <Data data={data} />;
}

我这样实现了useRemoteData:它只是测量自上次提取以来的时间。如果时间短于某个最小延迟,它会推迟对shouldFetch的调用。这也会使提取数据更有顺序性,这可能会导致总体加载时间略有增加,但我发现这不是一个大问题,两次提取之间的最大延迟为100ms。

const MIN_MILLIS_BETWEEN_FETCHES = 100;

interface RemoteDataExtras<S> {
    selected: S;
    isLoading: boolean;
}

interface RemoteDataOptions<P, S> {
    isLoading: (state: any, props: P) => boolean;
    select: (state: any, props: P) => S;
    shouldFetch: (state: any, props: P, extras: RemoteDataExtras<S>) => boolean;
    fetch: (dispatch: Dispatch<any>, state: any, props: P, extras: RemoteDataExtras<S>) => void;
}

const shared = { lastFetchMillis: 0 };

export function useRemoteData<P, S = unknown>(
    props: P,
    getOptions: () => RemoteDataOptions<P, S>
): [S, boolean, () => void] {
    const staticData = useRef({ timerId: -1 });
    const options = useMemo(getOptions, []);
    const state = useSelector(state => state);
    const [_, setRetryTime] = useState(0);
    const dispatch = useDispatch();

    const extras: RemoteDataExtras<S> = {
        selected: options.select(state, props),
        isLoading: options.isLoading(state, props),
    };

    const shouldFetch = () => {
        if (Date.now() - shared.lastFetchMillis < MIN_MILLIS_BETWEEN_FETCHES) {
            // The last fetch is too recent ago. Doing another fetch right now can
            // lead to major overfetching or even endless loops, because our
            // isLoading may not have updated yet due to the asynchronous way how
            // dispatch works. We return false here, but we add a timer to remind
            // us later.

            if (staticData.current.timerId >= 0) {
                window.clearTimeout(staticData.current.timerId);
            }

            const delay = MIN_MILLIS_BETWEEN_FETCHES -
                (Date.now() - shared.lastFetchMillis);

            staticData.current.timerId = window.setTimeout(() => {
                setRetryTime(Date.now()); // this will force a re-render, leading to a re-evaluation of shouldFetch
            }, delay);

            return false;
        } else {
            return options.shouldFetch(state, props, extras);
        }
    };

    const fetch = () => {
        options.fetch(dispatch, state, props, extras);
        shared.lastFetchMillis = Date.now();
    };

    useEffect(
        () => {
            if (shouldFetch()) {
                fetch();
            }
        },
        undefined // i.e. on each update
    );

    useEffect(() => {
        return () => {
            if (staticData.current.timerId >= 0) {
                window.clearTimeout(staticData.current.timerId);
            }
        };
    }, []);

    return [extras.selected, extras.isLoading, fetch];
}

相关问题