我努力完成这项工作,这是我认为的一种常见模式,但我还没有能够看到这方面的例子或解决方案。
以下是我正在处理的当前路线
/app/services/10/
app
中获取当前用户的登录信息/services
中提取用户可用的服务列表/10
中获取服务10的精细细节因此我使用某些数据填充商店的方式是:
import Services from './routes/Services'
export default (store) => ({
path: 'main',
getComponent (nextState, cb) {
require.ensure([], require => {
const App = require('./containers/AppContainer').default,
userActions = require('../store/user').actions
store.dispatch(userActions.fetch())
cb(null, App)
}, 'app')
},
childRoutes: [
Services(store)
]
})
现在问题出在childRoutes:
import { injectReducer } from '../../../../store/reducers'
import Manage from './routes/Manage'
export default (store) => ({
path: 'services',
getComponent (nextState, cb) {
require.ensure([], require => {
const Services = require('./containers/ServicesContainer').default
const actions = require('./modules/services').actions
const reducer = require('./modules/services').default
store.dispatch(actions.fetchAll())
injectReducer(store, { key: 'services', reducer })
cb(null, Services)
})
},
childRoutes: [
Manage(store)
]
})
正如您所见,childRoute Services
有fetchAll()
异步请求,您可以想象,需要来自store
的一些数据,特别是来自user
的数据商店中的属性,例如userId或令牌。
如果我自然导航,就不会有问题。但是当我刷新时,user
道具还没有填充。
如果你不能看到这是一个问题,作为我的路线的一部分:
app/services/10
参数10
需要来自store
的服务,
export default (store) => ({
path: ':id',
getComponent ({params: {id}}, cb) {
require.ensure([], require => {
const Manage = require('./containers/ManageContainer').default
const ServicesActions = require('../../modules/integrations').actions
store.dispatch(ServicesActions.selectService(id))
cb(null, Manage)
})
}
})
selectService
只是一个过滤掉state.services
问题是services
是异步提取的,当您刷新该路由时,即使在商店中的store.dispatch
完成并填充商店之前,services
仍会执行?
如何处理此异步问题?
答案 0 :(得分:13)
TL; DR :使用组件的生命周期挂钩在需要时获取数据,并有条件地呈现" loading"如果道具没有准备就说明。或者使用HoC以更可重用的方式封装此行为。
您的问题很有意思,因为它不仅与react-router有关,而且对于需要在呈现之前获取数据的react / redux应用程序。我们都在这个问题上至少挣了一次:"我在哪里获取数据?我如何知道数据是否已加载等等#34;。像Relay这样的问题框架试图解决这个问题。关于Relay的一个非常有趣的事情是,您可以为组件定义一些数据依赖关系,以便只有在数据有效且#34;时才能呈现它们。否则,一个" loading"状态呈现。
我们通常通过在componentDidMount
生命周期方法中获取所需数据来实现类似的结果,并且如果道具不是"有效且"有条件地渲染微调器。爱好。
在您的具体情况下,我理解正确,可以这样概括:
/services/
页面
ServicesContainer
加载了所有服务/services/10
,因为已经提取服务没有问题正如另一个答案所建议的那样,您可以通过在需要时获取数据来解决此问题,而不是在获取数据之前呈现服务。像这样:
class Services extends React.Component {
componentDidMount() {
if (!this.props.areServicesFetched) {
this.props.fetchServices()
}
}
render() {
return this.props.areServicesFetched ? (
<ul>
{this.props.services.map(service => <Service key={service.id} {...service}/>)}
</ul>
) : <p>{'Loading...'}</p>
}
}
const ServicesContainer = connect(
(state) => ({
areServicesFetched: areServicesFetched(state) // it's a selector, not shown in this example
services: getServices(state) // it's also a selector returning the services array or an empty array
}),
(dispatch) => ({
fetchServices() {
dispatch(fetchServices()) // let's say fetchServices is the async action that fetch services
}
})
)(Services)
const Service = ({ id, name }) => (
<li>{name}</li>
)
这很有效。如果对你来说足够了,你可以在这里停止阅读这个答案。如果您想要一种更好的可重用方法,请继续阅读。
在这个例子中,我们介绍了某种&#34;我的数据是否有效呈现,或者我怎样才能使它们有效?否则我们的组件内部会出现逻辑?&#34; 逻辑。如果我们想在不同的组件之间共享这个逻辑怎么办? As said by the doc:
在理想情况下,大多数组件都是无状态函数,因为将来我们还可以通过避免不必要的检查和内存分配来对这些组件进行性能优化。如果可能,这是推荐的模式。
我们可以理解的是,我们所有的组件都应该是纯粹的,而不是处理其他组件,也不是数据流(我的意思是数据流,&#34;我的数据是否被提取?&#34 ;等)。因此,让我们仅使用纯组件重写我们的示例,而不必担心现在的数据获取:
const Services = ({ services }) => (
<ul>
{services.map(service => <Service key={service.id} {...service}/>)}
</ul>
)
Services.propTypes = {
services: React.PropTypes.arrayOf(React.PropTypes.shape({
id: React.PropTypes.string,
}))
}
const Service = ({ id, name }) => (
<li>{name}</li>
)
Service.propTypes = {
id: React.PropTypes.string,
name: React.PropTypes.string
}
好的,到目前为止,我们有两个纯组件定义了他们需要的道具。就是这样。现在,我们需要在组件安装或渲染加载状态时根据需要提取&#34;获取数据&#34;某处。它是Higher-Order Component或HoC的完美角色。
简单来说,HoC可以让你将纯组件组合在一起,因为它们只不过是纯粹的功能。 HoC是一个函数,它将Component作为参数,并将该Component包装为另一个。
我们希望将服务的显示和获取它们的逻辑分开,因为正如我之前所说,您可能需要在另一个组件中获取服务的相同逻辑。 recompose是一个小库,为我们实现了一些非常有用的HoC。我们正在寻找
componentDidMount
生命周期方法<LoadingComponent>
services
组件提供<Services>
道具。因此,让我们构建我们的ensureServices
函数,该函数负责:
connect
纯组件到redux商店services
services
,则呈现加载状态services
后呈现我们的组件这是一个实现:
const ensureServices = (PureComponent, LoadingComponent) => {
/* below code is taken from recompose doc https://github.com/acdlite/recompose/blob/master/docs/API.md#rendercomponent */
const identity = t => t
// `hasLoaded()` is a function that returns whether or not the the component
// has all the props it needs
const spinnerWhileLoading = hasLoaded =>
branch(
hasLoaded,
identity, // Component => Component
renderComponent(LoadingComponent) // <LoadingComponent> is a React component
)
/* end code taken from recompose doc */
return connect(
(state) => ({
areAllServicesFetched: areAllServicesFetched(state), // some selector...
services: getServices(state) //some selector
}),
(dispatch) => ({
fetchServices: dispatch(fetchServices())
})
)(compose(
lifecycle({
componentDidMount() {
if (!this.props.areAllServicesFetched) {
this.props.fetchServices()
}
}
}),
spinnerWhileLoading(props => props.areAllServicesFetched),
mapProps(props => ({ services: props.services }))
)(PureComponent))
}
现在,只要某个组件需要商店中的services
,我们就可以像这样使用它:
const Loading = () => <p>Loading...</p>
const ServicesContainer = ensureServices(Services, Loading)
在这里,我们的<Services>
组件只显示服务,但如果您有一个<ServicesForm>
组件需要services
来为每个服务呈现输入,我们可以写一些像:
const ServicesFormContainer = ensureServices(ServicesForm, Loading)
如果你不想概括这个模式,你可以看一下react-redux-pledge,我拥有的一个小型库来处理这种数据依赖。
答案 1 :(得分:2)
我对我所使用的应用程序进行了相当多的讨论。您似乎正在使用React Router - 如果是这种情况,您可以利用onEnter
/ onChange
挂钩。
API文档位于:https://github.com/reactjs/react-router/blob/master/docs/API.md#onenternextstate-replace-callback
您可以使用getComponent
挂钩并使用回调参数(就像您使用onEnter
一样)来表示异步getComponent
方法中的数据,而不是在异步export default (store) => ({
path: ':id',
getComponent ({params: {id}}, cb) {
require.ensure([], require => {
const Manage = require('./containers/ManageContainer').default
const ServicesActions = require('../../modules/integrations').actions
cb(null, Manage)
})
},
onEnter: (nextState, replace, cb) => {
const actions = require('./modules/services').actions
const reducer = require('./modules/services').default
//fetch async data
store.dispatch(actions.fetchAll()).then(() => {
//after you've got the data, fire selectService method (assuming it is synchronous)
const ServicesActions = require('../../modules/integrations').actions
store.dispatch(ServicesActions.selectService(id))
cb()//this tells react-router we've loaded all data
})
}
})
方法中加载数据react-router应该阻止加载此路由,直到加载数据。
如果你使用的是redux-thunk:
,这样的话就可以了componentDidMount
我发现使用路由器挂钩加载数据的模式是一种非常干净的方式,可以确保组件所需的所有数据都存在。如有必要,它也是拦截未经身份验证的用户的好方法。
另一种方法是在组件的rails app:update:bin
方法中显式加载数据。