有没有办法链接react-redux和react-router?

时间:2019-09-07 16:47:46

标签: reactjs react-redux react-router

我有一个使用url参数更改页面上显示数据的应用程序。

目前,我正在使用react-router和react-redux。 react-router将参数传递给路由中的组件:

<Route path='/annotations/:sessionId/:eventId' render={route => 
       <AnnotationPage sessionId={route.match.params.sessionId} eventId={route.match.params.eventId} />} />

然后,该组件将这些值传递到我的ActionCreator,后者调用服务器并根据传入的sessionIdeventId更新状态。

我看到的问题:

  • 有人可以调用ActionCreator并更改eventIdsessionId而不用更新URL参数
  • 现在存储了两个ID。处于状态(由ActionCreator设置的一个)和处于URL中的一个

我希望URL参数成为我的应用程序中的“真理之源”,但是我不确定这样做的优雅方式。有人有什么想法吗?

3 个答案:

答案 0 :(得分:1)

location发生更改时,react-router-redux将分派类型为"@@router/LOCATION_CHANGE"的动作。此字符串将导出为LOCATION_CHANGE,因此可以导入:

import { LOCATION_CHANGE } from 'react-router-redux'

reduce可以处理此操作并通过位置来增强状态:

case LOCATION_CHANGE:
  return { ...state, ...action.payload };

然后,URL参数作为Redux状态的一部分,成为应用程序中的“真理之源”。

P.S。

  • react-router未调度LOCATION_CHANGE操作。
  • react-router-redux确实调度了LOCATION_CHANGE操作,因此您可以拦截它并确定要添加到Redux存储中的数据片段(包含在该操作的payload中)。但是路由器不再维护。
  • connected-react-router确实调度了LOCATION_CHANGE动作。您不希望拦截此操作以使用其payload中包含的数据来扩展Redux存储,因为路由器还是这样做。也就是说,它会自动扩充Redux存储。但是您可能想要拦截LOCATION_CHANGE动作以用于其他目的。 redux-saga可以使用其take/takeEvery/takeLatest/takeLeading来拦截LOCATION_CHANGE操作,并以您选择的一个或多个异步和非异步操作进行响应,例如获取,将其他操作分派到Redux存储等。 。

答案 1 :(得分:0)

这是一个基于逻辑的问题。

由于您和您的开发人员都在控制代码,因此,您最好的办法就是记录该动作创建者,并确保仅使用从URL获得的参数来调用它。尝试将URL作为source of truth绑定到代码上,仍然可能导致出现重复的数据eventid和sessionid的情况,即用户更改了URL参数并刷新了页面。因此,您应该在服务器上执行检查,以确保eventid和sessionid来自正确的来源并且未被操纵。或者没有存储两个ID。

答案 2 :(得分:0)

想为可能遇到它的任何人更新它。我最终实现了与建议的@ winwiz1类似的东西,但更具体地connected-react-router

我曾经无法进行位置更改来触发服务器请求,但是通过超时检查过滤器变量何时更改以及使过滤器变量根据URL进行更改,我得到了我想要的东西。

store.ts

import { LocationChangeAction } from 'connected-react-router';
...
export type FilterActions = 
    | LocationChangeAction
...
export const placeItemListReducer: Reducer<IPlaceItemListState, FilterActions> = (
    state = initialPlaceListState,
    action,
) => {
    switch (action.type) {
        case '@@router/LOCATION_CHANGE': {
            if (placePageRegex.test(action.payload.location.pathname)) {
                const toParse: string = action.payload.location.pathname
                    .replace('/order', '')
                    .replace('/menu', '')
                    .replace('/edit', '');
                const newFsiUrl: string = toParse.substring(toParse.lastIndexOf('/') + 1);

                if (newFsiUrl === state.placefsiUrlFilter && 
                    state.version === currStoreVersion) {
                    return state;
                }
                else {
                    return {
                        ...state,
                        version: currStoreVersion,
                        placefsiUrlFilter: newFsiUrl,
                        place: null,
                        placeItems: [],
                        lastFilterChangeEpochMilliseconds: Date.now(),
                        productsPerPage: stringNullEmptyOrUndefined(newFsiUrl) ? state.productsPerPage : 500,
                    };
                }
            }
            else {
                return state;
            }
        }
...

sync.ts

export const startSyncIntervalActionCreator: ActionCreator<
    ThunkAction<
        Promise<void>,      // The type of the last action to be dispatched - will always be promise<T> for async actions
        IAppState,          // The type for the data within the last action
        string,             // The type of the parameter for the nested function 
        ISyncSuccessAction  // The type of the last action to be dispatched
    >
> = () => {
    return async (dispatch: ThunkDispatch<any, any, AnyAction>) => {
        if (timer !== undefined) {
            return;
        }
        
        timer = setInterval(() => dispatch(syncActionCreator()), 100);
    };
};
...
export const syncActionCreator: ActionCreator<
    ThunkAction<
        Promise<void>,      // The type of the last action to be dispatched - will always be promise<T> for async actions
        IAppState,          // The type for the data within the last action
        string,             // The type of the parameter for the nested function 
        ISyncSuccessAction  // The type of the last action to be dispatched
    >
> = () => {
    return async (dispatch: ThunkDispatch<any, any, AnyAction>, getState: () => IAppState) => {
        if (getState().syncState.currentlySyncing) {
            return;
        }

        const millisecondsSinceLastSync: number = (getCurrentTimeEpochMilliseconds() - getState().placeItemListState.lastSyncEpochMilliseconds);
        const hasDataTimeout: boolean = millisecondsSinceLastSync > dataTimeoutPeriodMilliseconds;
        if (getState().placeItemListState.lastSyncEpochMilliseconds <= getState().placeItemListState.lastFilterChangeEpochMilliseconds ||
                hasDataTimeout) {
            const startPlaceItemSyncAction: IStartPlaceItemSyncAction = {
                type: 'StartPlaceItemSync',
                fromDataTimeout: hasDataTimeout,
            };
            dispatch(startPlaceItemSyncAction);

            axios.post(
                process.env.REACT_APP_API_ROOT_URL + 'PlaceItem/sync-flat',
                {
                    fsiUrlFilter: getState().placeItemListState.placefsiUrlFilter,
                    searchTerms: getState().searchTermsState.placeItemSearchTerms,
                    tagIds: getState().toggleFilterState.placeItemTags.filter(f => f.selected).map(f => f.id),
                    pageNumber: getState().placeItemListState.pageNum,
                    productsPerPage: getState().placeItemListState.productsPerPage,
                },
                {
                    headers: { 
                        'Access-Control-Allow-Origin': '*',
                        'Authorization': `Bearer ${getState().authState.authToken}`,
                    }
                }
            ).then((res: AxiosResponse<IPlaceItemSyncFlatResponse>) => {
                const syncResponse: IPlaceItemSyncFlatResponse = res.data;
                const syncSuccessAction: IPlaceItemSyncSuccessAction = {
                    type: 'PlaceItemSyncSuccess',
                    syncTimestampEpochMilliseconds: startSyncTime,
                    placeData: res.data.placeData,
                    placeItems: syncResponse.placeItems,
                    extraCategories: syncResponse.extraCategories,
                    extras: syncResponse.extras,
                    currentMenuCategories: syncResponse.menuCategories,
                    tags: syncResponse.tags,
                    totalNumberOfPlaces: syncResponse.totalNumberOfPlaces,
                    restaurantOrders: syncResponse.orderFromLast5Days,
                };
                dispatch(syncSuccessAction);
            });
        }
    }
};

syncWrapper.ts(确保应用程序在启动同步超时时被包装在此组件中)

...
interface IProps {
    children: React.ReactNode;
    startSync: () => void,
}

const SyncWrapper: React.FC<IProps> = ({
    children,
    startSync,
}) => {
    startSync();

    return (
        <>{children}</>
    );
}

const mapStateToProps = (store: IOfflineAppState) => {
    return {
    };
};

const mapDispatchToProps = (dispatch: ThunkDispatch<any, any, AnyAction>) => {
    return {
        startSync: () => dispatch(startSyncIntervalActionCreator()),
    };
};

export default connect(
    mapStateToProps,
    mapDispatchToProps,
)(SyncWrapper);