React-Router:如何在路由转换之前等待异步操作
React-Router: how to wait for an async action before route transition
是否可以在特定路由上调用称为 thunk
的异步 redux 操作,并且在响应成功或失败之前不执行转换?
用例
我们需要从服务器加载数据并用初始值填写表单。在从服务器获取数据之前,这些初始值不存在。
像这样的语法会很棒:
<Route path="/myForm" component={App} async={dispatch(loadInitialFormValues(formId))}>
回答在响应成功或失败之前阻止转换到新路由的原始问题:
因为您使用的是 redux thunk,您可能会在操作创建者中成功或失败触发重定向。我不知道你的具体动作/动作创建者是什么样子,但这样的东西可能有效:
import { browserHistory } from 'react-router'
export function loadInitialFormValues(formId) {
return function(dispatch) {
// hit the API with some function and return a promise:
loadInitialValuesReturnPromise(formId)
.then(response => {
// If request is good update state with fetched data
dispatch({ type: UPDATE_FORM_STATE, payload: response });
// - redirect to the your form
browserHistory.push('/myForm');
})
.catch(() => {
// If request is bad...
// do whatever you want here, or redirect
browserHistory.push('/myForm')
});
}
}
跟进。在进入路由/在组件的 componentWillMount 上加载数据并显示微调器的常见模式:
来自关于异步操作的 redux 文档 http://redux.js.org/docs/advanced/AsyncActions.html
- An action informing the reducers that the request began.
The reducers may handle this action by toggling an isFetching flag in
the state. This way the UI knows it’s time to show a spinner.
- An action informing the reducers that the request finished successfully.
The reducers may handle this action by merging the new data into the
state they manage and resetting isFetching. The UI would hide the
spinner, and display the fetched data.
- An action informing the reducers that the request failed.
The reducers may handle this action by resetting isFetching.
Additionally, some reducers may want to store the error message so the
UI can display it.
我以您的情况为粗略指南,遵循了下面的一般模式。你不必使用承诺
// action creator:
export function fetchFormData(formId) {
return dispatch => {
// an action to signal the beginning of your request
// this is what eventually triggers the displaying of the spinner
dispatch({ type: FETCH_FORM_DATA_REQUEST })
// (axios is just a promise based HTTP library)
axios.get(`/formdata/${formId}`)
.then(formData => {
// on successful fetch, update your state with the new form data
// you can also turn these into their own action creators and dispatch the invoked function instead
dispatch({ type: actions.FETCH_FORM_DATA_SUCCESS, payload: formData })
})
.catch(error => {
// on error, do whatever is best for your use case
dispatch({ type: actions.FETCH_FORM_DATA_ERROR, payload: error })
})
}
}
// reducer
const INITIAL_STATE = {
formData: {},
error: {},
fetching: false
}
export default function(state = INITIAL_STATE, action) {
switch(action.type) {
case FETCH_FORM_DATA_REQUEST:
// when dispatch the 'request' action, toggle fetching to true
return Object.assign({}, state, { fetching: true })
case FETCH_FORM_DATA_SUCCESS:
return Object.assign({}, state, {
fetching: false,
formData: action.payload
})
case FETCH_FORM_DATA_ERROR:
return Object.assign({}, state, {
fetching: false,
error: action.payload
})
}
}
// route can look something like this to access the formId in the URL if you want
// I use this URL param in the component below but you can access this ID anyway you want:
<Route path="/myForm/:formId" component={SomeForm} />
// form component
class SomeForm extends Component {
componentWillMount() {
// get formId from route params
const formId = this.props.params.formId
this.props.fetchFormData(formId)
}
// in render just check if the fetching process is happening to know when to display the spinner
// this could also be abstracted out into another method and run like so: {this.showFormOrSpinner.call(this)}
render() {
return (
<div className="some-form">
{this.props.fetching ?
<img src="./assets/spinner.gif" alt="loading spinner" /> :
<FormComponent formData={this.props.formData} />
}
</div>
)
}
}
function mapStateToProps(state) {
return {
fetching: state.form.fetching,
formData: state.form.formData,
error: state.form.error
}
}
export default connect(mapStateToProps, { fetchFormData })(SomeForm)
首先,我想说的是is a debate around题目用react-router的onEnter
hooks抓取数据好不好练习,不过这就是这样的事情:
您可以将 redux-store 传递给您的 Router
。让以下成为您的根组件,其中安装了 Router
:
...
import routes from 'routes-location';
class Root extends React.Component {
render() {
const { store, history } = this.props;
return (
<Provider store={store}>
<Router history={history}>
{ routes(store) }
</Router>
</Provider>
);
}
}
...
你的路线将是这样的:
import ...
...
const fetchData = (store) => {
return (nextState, transition, callback) => {
const { dispatch, getState } = store;
const { loaded } = getState().myCoolReduxStore;
// loaded is a key from my store that I put true when data has loaded
if (!loaded) {
// no data, dispatch action to get it
dispatch(getDataAction())
.then((data) => {
callback();
})
.catch((error) => {
// maybe it failed because of 403 forbitten, we can use tranition to redirect.
// what's in state will come as props to the component `/forbitten` will mount.
transition({
pathname: '/forbitten',
state: { error: error }
});
callback();
});
} else {
// we already have the data loaded, let router continue its transition to the route
callback();
}
}
};
export default (store) => {
return (
<Route path="/" component={App}>
<Route path="myPage" name="My Page" component={MyPage} onEnter={fetchData(store)} />
<Route path="forbitten" name="403" component={PageForbitten} />
<Route path="*" name="404" component={PageNotFound} />
</Route>
);
};
请注意,您的路由器文件正在导出一个以您的商店作为参数的 thunk,如果您向上看,请看我们如何调用路由器,我们将商店对象传递给它。
遗憾的是,在写 react-router docs return 404 给我的时候,我无法向您指出描述 (nextState, transition, callback)
的文档。但是,关于那些,根据我的记忆:
nextState
描述了react-router
将过渡到的路线;
transition
函数执行可能不同于 nextState
;
的另一个转换
callback
将触发您的路线转换完成。
另一个需要指出的是,使用 redux-thunk,你的调度操作可以 return 一个承诺,在文档中检查它 here. You can find here 关于如何配置你的 redux 存储的一个很好的例子redux-thunk.
我为此制作了一个方便的钩子,适用于 react-router v5:
/*
* Return truthy if you wish to block. Empty return or false will not block
*/
export const useBlock = func => {
const { block, push, location } = useHistory()
const lastLocation = useRef()
const funcRef = useRef()
funcRef.current = func
useEffect(() => {
if (location === lastLocation.current || !funcRef.current)
return
lastLocation.current = location
const unblock = block((location, action) => {
const doBlock = async () => {
if (!(await funcRef.current(location, action))) {
unblock()
push(location)
}
}
doBlock()
return false
})
}, [location, block, push])
}
在你的组件中,像这样使用它:
const MyComponent = () => {
useBlock(async location => await fetchShouldBlock(location))
return <span>Hello</span>
}
直到异步函数 returns 才会出现导航;您可以通过返回 true
.
来完全阻止导航
是否可以在特定路由上调用称为 thunk
的异步 redux 操作,并且在响应成功或失败之前不执行转换?
用例
我们需要从服务器加载数据并用初始值填写表单。在从服务器获取数据之前,这些初始值不存在。
像这样的语法会很棒:
<Route path="/myForm" component={App} async={dispatch(loadInitialFormValues(formId))}>
回答在响应成功或失败之前阻止转换到新路由的原始问题:
因为您使用的是 redux thunk,您可能会在操作创建者中成功或失败触发重定向。我不知道你的具体动作/动作创建者是什么样子,但这样的东西可能有效:
import { browserHistory } from 'react-router'
export function loadInitialFormValues(formId) {
return function(dispatch) {
// hit the API with some function and return a promise:
loadInitialValuesReturnPromise(formId)
.then(response => {
// If request is good update state with fetched data
dispatch({ type: UPDATE_FORM_STATE, payload: response });
// - redirect to the your form
browserHistory.push('/myForm');
})
.catch(() => {
// If request is bad...
// do whatever you want here, or redirect
browserHistory.push('/myForm')
});
}
}
跟进。在进入路由/在组件的 componentWillMount 上加载数据并显示微调器的常见模式:
来自关于异步操作的 redux 文档 http://redux.js.org/docs/advanced/AsyncActions.html
- An action informing the reducers that the request began.
The reducers may handle this action by toggling an isFetching flag in the state. This way the UI knows it’s time to show a spinner.
- An action informing the reducers that the request finished successfully.
The reducers may handle this action by merging the new data into the state they manage and resetting isFetching. The UI would hide the spinner, and display the fetched data.
- An action informing the reducers that the request failed.
The reducers may handle this action by resetting isFetching. Additionally, some reducers may want to store the error message so the UI can display it.
我以您的情况为粗略指南,遵循了下面的一般模式。你不必使用承诺
// action creator:
export function fetchFormData(formId) {
return dispatch => {
// an action to signal the beginning of your request
// this is what eventually triggers the displaying of the spinner
dispatch({ type: FETCH_FORM_DATA_REQUEST })
// (axios is just a promise based HTTP library)
axios.get(`/formdata/${formId}`)
.then(formData => {
// on successful fetch, update your state with the new form data
// you can also turn these into their own action creators and dispatch the invoked function instead
dispatch({ type: actions.FETCH_FORM_DATA_SUCCESS, payload: formData })
})
.catch(error => {
// on error, do whatever is best for your use case
dispatch({ type: actions.FETCH_FORM_DATA_ERROR, payload: error })
})
}
}
// reducer
const INITIAL_STATE = {
formData: {},
error: {},
fetching: false
}
export default function(state = INITIAL_STATE, action) {
switch(action.type) {
case FETCH_FORM_DATA_REQUEST:
// when dispatch the 'request' action, toggle fetching to true
return Object.assign({}, state, { fetching: true })
case FETCH_FORM_DATA_SUCCESS:
return Object.assign({}, state, {
fetching: false,
formData: action.payload
})
case FETCH_FORM_DATA_ERROR:
return Object.assign({}, state, {
fetching: false,
error: action.payload
})
}
}
// route can look something like this to access the formId in the URL if you want
// I use this URL param in the component below but you can access this ID anyway you want:
<Route path="/myForm/:formId" component={SomeForm} />
// form component
class SomeForm extends Component {
componentWillMount() {
// get formId from route params
const formId = this.props.params.formId
this.props.fetchFormData(formId)
}
// in render just check if the fetching process is happening to know when to display the spinner
// this could also be abstracted out into another method and run like so: {this.showFormOrSpinner.call(this)}
render() {
return (
<div className="some-form">
{this.props.fetching ?
<img src="./assets/spinner.gif" alt="loading spinner" /> :
<FormComponent formData={this.props.formData} />
}
</div>
)
}
}
function mapStateToProps(state) {
return {
fetching: state.form.fetching,
formData: state.form.formData,
error: state.form.error
}
}
export default connect(mapStateToProps, { fetchFormData })(SomeForm)
首先,我想说的是is a debate around题目用react-router的onEnter
hooks抓取数据好不好练习,不过这就是这样的事情:
您可以将 redux-store 传递给您的 Router
。让以下成为您的根组件,其中安装了 Router
:
...
import routes from 'routes-location';
class Root extends React.Component {
render() {
const { store, history } = this.props;
return (
<Provider store={store}>
<Router history={history}>
{ routes(store) }
</Router>
</Provider>
);
}
}
...
你的路线将是这样的:
import ...
...
const fetchData = (store) => {
return (nextState, transition, callback) => {
const { dispatch, getState } = store;
const { loaded } = getState().myCoolReduxStore;
// loaded is a key from my store that I put true when data has loaded
if (!loaded) {
// no data, dispatch action to get it
dispatch(getDataAction())
.then((data) => {
callback();
})
.catch((error) => {
// maybe it failed because of 403 forbitten, we can use tranition to redirect.
// what's in state will come as props to the component `/forbitten` will mount.
transition({
pathname: '/forbitten',
state: { error: error }
});
callback();
});
} else {
// we already have the data loaded, let router continue its transition to the route
callback();
}
}
};
export default (store) => {
return (
<Route path="/" component={App}>
<Route path="myPage" name="My Page" component={MyPage} onEnter={fetchData(store)} />
<Route path="forbitten" name="403" component={PageForbitten} />
<Route path="*" name="404" component={PageNotFound} />
</Route>
);
};
请注意,您的路由器文件正在导出一个以您的商店作为参数的 thunk,如果您向上看,请看我们如何调用路由器,我们将商店对象传递给它。
遗憾的是,在写 react-router docs return 404 给我的时候,我无法向您指出描述 (nextState, transition, callback)
的文档。但是,关于那些,根据我的记忆:
nextState
描述了react-router
将过渡到的路线;transition
函数执行可能不同于nextState
; 的另一个转换
callback
将触发您的路线转换完成。
另一个需要指出的是,使用 redux-thunk,你的调度操作可以 return 一个承诺,在文档中检查它 here. You can find here 关于如何配置你的 redux 存储的一个很好的例子redux-thunk.
我为此制作了一个方便的钩子,适用于 react-router v5:
/*
* Return truthy if you wish to block. Empty return or false will not block
*/
export const useBlock = func => {
const { block, push, location } = useHistory()
const lastLocation = useRef()
const funcRef = useRef()
funcRef.current = func
useEffect(() => {
if (location === lastLocation.current || !funcRef.current)
return
lastLocation.current = location
const unblock = block((location, action) => {
const doBlock = async () => {
if (!(await funcRef.current(location, action))) {
unblock()
push(location)
}
}
doBlock()
return false
})
}, [location, block, push])
}
在你的组件中,像这样使用它:
const MyComponent = () => {
useBlock(async location => await fetchShouldBlock(location))
return <span>Hello</span>
}
直到异步函数 returns 才会出现导航;您可以通过返回 true
.