我有一个使用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
,后者调用服务器并根据传入的sessionId
和eventId
更新状态。
我看到的问题:
ActionCreator
并更改eventId
或sessionId
而不用更新URL参数ActionCreator
设置的一个)和处于URL中的一个我希望URL参数成为我的应用程序中的“真理之源”,但是我不确定这样做的优雅方式。有人有什么想法吗?
答案 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。
LOCATION_CHANGE
操作。LOCATION_CHANGE
操作,因此您可以拦截它并确定要添加到Redux存储中的数据片段(包含在该操作的payload
中)。但是路由器不再维护。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);