【问题标题】:React-Router: how to wait for an async action before route transitionReact-Router:如何在路由转换之前等待异步操作
【发布时间】:2016-10-17 10:11:06
【问题描述】:

是否可以在特定路由上调用称为 thunk 的异步 redux 操作,并且在响应成功或失败之前不执行转换?

用例

我们需要从服务器加载数据并用初始值填写表格。在从服务器获取数据之前,这些初始值不存在。

这样的语法会很棒:

<Route path="/myForm" component={App} async={dispatch(loadInitialFormValues(formId))}>

【问题讨论】:

  • 预期的路线变化何时发生?是来自某人单击链接还是按钮?还是别的什么?
  • 好吧,其中任何一个:按钮、编程、页面加载等。所有这些情况。

标签: reactjs state redux react-router react-router-redux


【解决方案1】:

要回答在响应成功或失败之前阻止转换到新路由的原始问题:

因为您使用的是 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

  • 通知reducer请求开始的动作。

reducer 可以通过切换一个 isFetching 标志来处理这个动作 国家。这样,UI 就知道是时候显示微调器了。

  • 通知reducer请求成功完成的动作。

reducer 可以通过将新数据合并到 声明他们管理并重置 isFetching。 UI 会隐藏 微调器,并显示获取的数据。

  • 通知reducers请求失败的操作。

reducer 可以通过重置 isFetching 来处理这个动作。 此外,一些 reducer 可能希望存储错误消息,以便 UI 可以显示。

我按照以下一般模式使用您的情况作为粗略指导。你不必使用承诺

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

【讨论】:

  • 这似乎是一种反模式。我在想所有匹配模式的路由都可以触发状态的重新加载以保持整个状态树自动更新
  • 您还可以在路由上有一个 onEnter 事件处理程序,或者在 componentWillMount 上获取数据并在获取表单数据时使用微调器,而不是冻结导航。这是 Redux Thunk 的一个非常常见的用途
  • 这似乎是我所追求的想法 :-) 把它作为答案?
  • @AndrewMcLagan 好的,很酷,这是一种非常常见的模式,我已经做了很多,所以我现在为你写一个例子
  • React Router v4 的任何解决方案?
【解决方案2】:

首先,我想说is a debate around 使用react-router 的onEnter 钩子获取数据的主题是否是好的做法,但这是怎么回事会去:

您可以将 redux-store 传递给您的 Router。让以下内容成为您的 Root 组件,其中安装了 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 时,返回 404 给我,因此我无法向您指出描述 (nextState, transition, callback) 的文档。但是,关于那些,根据我的记忆:

  • nextState 描述了react-router 将转换到的路由;

  • transition 函数执行的转换可能不是来自nextState 的转换;

  • callback 将触发您的路线转换完成。

另一个需要指出的是,使用 redux-thunk,您的调度操作可以返回一个承诺,请在文档 here 中查看。你可以找到 here 一个很好的例子,了解如何使用 redux-thunk 配置你的 redux 存储。

【讨论】:

  • 需要注意的是,你提到的“辩论”现在已经很过时了,从未得到解决,澄清声明见this issue
【解决方案3】:

为此我做了一个方便的钩子,与 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>
}

在异步函数返回之前不会进行导航;你可以通过返回true来完全阻止导航。

【讨论】:

  • 很高兴看到带有过时答案的现代代码的新答案。感谢分享!
猜你喜欢
  • 1970-01-01
  • 2016-09-21
  • 2018-03-18
  • 2017-10-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-26
  • 1970-01-01
相关资源
最近更新 更多