SO Relay QueryRenderer语言 - TypeError: this.props.render 不是一个函数



我在React中使用ReduxRelay有一个项目。客户端使用 GraphQL 连接到 API 服务器。我正在尝试使用组件查询渲染器,但出现以下错误:

TypeError: this.props.render is not a function
render
src/react-landing/node_modules/react-relay/lib/ReactRelayQueryRenderer.js:164
161 |   if (process.env.NODE_ENV !== 'production') {
162 |     deepFreeze(renderProps);
163 |   }
> 164 |   return this.props.render(renderProps);
165 | };
166 | 
167 | return ReactRelayQueryRenderer;
View compiled
finishClassComponent
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:13193
13190 | } else {
13191 |   {
13192 |     ReactDebugCurrentFiber.setCurrentPhase('render');
> 13193 |     nextChildren = instance.render();
13194 |     if (debugRenderPhaseSideEffects || debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) {
13195 |       instance.render();
13196 |     }
View compiled
updateClassComponent
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:13155
13152 |   } else {
13153 |     shouldUpdate = updateClassInstance(current, workInProgress, renderExpirationTime);
13154 |   }
> 13155 |   return finishClassComponent(current, workInProgress, shouldUpdate, hasContext, renderExpirationTime);
13156 | }
13157 | 
13158 | function finishClassComponent(current, workInProgress, shouldUpdate, hasContext, renderExpirationTime) {
View compiled
beginWork
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:13824
13821 | case FunctionalComponent:
13822 |   return updateFunctionalComponent(current, workInProgress);
13823 | case ClassComponent:
> 13824 |   return updateClassComponent(current, workInProgress, renderExpirationTime);
13825 | case HostRoot:
13826 |   return updateHostRoot(current, workInProgress, renderExpirationTime);
13827 | case HostComponent:
View compiled
performUnitOfWork
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:15863
15860 |   startBaseRenderTimer();
15861 | }
15862 | 
> 15863 | next = beginWork(current, workInProgress, nextRenderExpirationTime);
15864 | 
15865 | if (workInProgress.mode & ProfileMode) {
15866 |   // Update "base" time if the render wasn't bailed out on.
View compiled
workLoop
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:15902
15899 | if (!isAsync) {
15900 |   // Flush all expired work.
15901 |   while (nextUnitOfWork !== null) {
> 15902 |     nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
15903 |   }
15904 | } else {
15905 |   // Flush asynchronous work until the deadline runs out of time.
View compiled
callCallback
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:100
97 |   // nested call would trigger the fake event handlers of any call higher
98 |   // in the stack.
99 |   fakeNode.removeEventListener(evtType, callCallback, false);
> 100 |   func.apply(context, funcArgs);
101 |   didError = false;
102 | }
103 | 
View compiled
invokeGuardedCallbackDev
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:138
135 | // Synchronously dispatch our fake event. If the user-provided function
136 | // errors, it will trigger our global error handler.
137 | evt.initEvent(evtType, false, false);
> 138 | fakeNode.dispatchEvent(evt);
139 | 
140 | if (didError) {
141 |   if (!didSetError) {
View compiled
invokeGuardedCallback
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:187
184 |  * @param {...*} args Arguments for function
185 |  */
186 | invokeGuardedCallback: function (name, func, context, a, b, c, d, e, f) {
> 187 |   invokeGuardedCallback$1.apply(ReactErrorUtils, arguments);
188 | },
189 | 
190 | /**
View compiled
replayUnitOfWork
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:15310
15307 | // Replay the begin phase.
15308 | isReplayingFailedUnitOfWork = true;
15309 | originalReplayError = thrownValue;
> 15310 | invokeGuardedCallback$2(null, workLoop, null, isAsync);
15311 | isReplayingFailedUnitOfWork = false;
15312 | originalReplayError = null;
15313 | if (hasCaughtError()) {
View compiled
renderRoot
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:15962
15959 | 
15960 | var failedUnitOfWork = nextUnitOfWork;
15961 | if (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
> 15962 |   replayUnitOfWork(failedUnitOfWork, thrownValue, isAsync);
15963 | }
15964 | 
15965 | // TODO: we already know this isn't true in some cases.
View compiled
performWorkOnRoot
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16560
16557 |   // This root is already complete. We can commit it.
16558 |   completeRoot(root, finishedWork, expirationTime);
16559 | } else {
> 16560 |   finishedWork = renderRoot(root, expirationTime, false);
16561 |   if (finishedWork !== null) {
16562 |     // We've completed the root. Commit it.
16563 |     completeRoot(root, finishedWork, expirationTime);
View compiled
performWork
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16482
16479 |   }
16480 | } else {
16481 |   while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && (minExpirationTime === NoWork || minExpirationTime >= nextFlushedExpirationTime)) {
> 16482 |     performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);
16483 |     findHighestPriorityRoot();
16484 |   }
16485 | }
View compiled
performSyncWork
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16454
16451 | }
16452 | 
16453 | function performSyncWork() {
> 16454 |   performWork(Sync, false, null);
16455 | }
16456 | 
16457 | function performWork(minExpirationTime, isAsync, dl) {
View compiled
requestWork
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16354
16351 | 
16352 | // TODO: Get rid of Sync and use current time?
16353 | if (expirationTime === Sync) {
> 16354 |   performSyncWork();
16355 | } else {
16356 |   scheduleCallbackWithExpiration(expirationTime);
16357 | }
View compiled
scheduleWork$1
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16218
16215 | !isWorking || isCommitting$1 ||
16216 | // ...unless this is a different root than the one we're rendering.
16217 | nextRoot !== root) {
> 16218 |   requestWork(root, nextExpirationTimeToWorkOn);
16219 | }
16220 | if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
16221 |   invariant(false, 'Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.');
View compiled
scheduleRootUpdate
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16785
16782 |   }
16783 |   enqueueUpdate(current, update, expirationTime);
16784 | 
> 16785 |   scheduleWork$1(current, expirationTime);
16786 |   return expirationTime;
16787 | }
16788 | 
View compiled
updateContainerAtExpirationTime
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16812
16809 |     container.pendingContext = context;
16810 |   }
16811 | 
> 16812 |   return scheduleRootUpdate(current, element, expirationTime, callback);
16813 | }
16814 | 
16815 | function findHostInstance(component) {
View compiled
updateContainer
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16839
16836 |   var current = container.current;
16837 |   var currentTime = recalculateCurrentTime();
16838 |   var expirationTime = computeExpirationForFiber(currentTime, current);
> 16839 |   return updateContainerAtExpirationTime(element, container, parentComponent, expirationTime, callback);
16840 | }
16841 | 
16842 | function getPublicRootInstance(container) {
View compiled
./node_modules/react-dom/cjs/react-dom.development.js/ReactRoot.prototype.render
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:17122
17119 |   if (callback !== null) {
17120 |     work.then(callback);
17121 |   }
> 17122 |   updateContainer(children, root, null, work._onCommit);
17123 |   return work;
17124 | };
17125 | ReactRoot.prototype.unmount = function (callback) {
View compiled
legacyRenderSubtreeIntoContainer/<
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:17262
17259 |     if (parentComponent != null) {
17260 |       root.legacy_renderSubtreeIntoContainer(parentComponent, children, callback);
17261 |     } else {
> 17262 |       root.render(children, callback);
17263 |     }
17264 |   });
17265 | } else {
View compiled
unbatchedUpdates
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:16679
16676 |       isUnbatchingUpdates = false;
16677 |     }
16678 |   }
> 16679 |   return fn(a);
16680 | }
16681 | 
16682 | // TODO: Batching should be implemented at the renderer level, not within
View compiled
legacyRenderSubtreeIntoContainer
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:17258
17255 |   };
17256 | }
17257 | // Initial mount should not be batched.
> 17258 | unbatchedUpdates(function () {
17259 |   if (parentComponent != null) {
17260 |     root.legacy_renderSubtreeIntoContainer(parentComponent, children, callback);
17261 |   } else {
View compiled
render
src/react-landing/node_modules/react-dom/cjs/react-dom.development.js:17317
17314 |   return legacyRenderSubtreeIntoContainer(null, element, container, true, callback);
17315 | },
17316 | render: function (element, container, callback) {
> 17317 |   return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
17318 | },
17319 | unstable_renderSubtreeIntoContainer: function (parentComponent, element, containerNode, callback) {
17320 |   !(parentComponent != null && has(parentComponent)) ? invariant(false, 'parentComponent must be a valid React Component') : void 0;
View compiled
./src/index.js
src/react-landing/src/index.js:15
12 | import '../node_modules/font-awesome/css/font-awesome.min.css';
13 | 
14 | 
> 15 | ReactDOM.render(
16 |   <Provider store={ store }>
17 |     <I18nextProvider i18n={ i18n }>
18 |       <App />
View compiled
▶ 6 stack frames were collapsed.

以下是源文件:

src/components/homePage/Header/Header.jsx

import React from 'react';
import { connect } from "react-redux";
import { I18n } from 'react-i18next';
import { QueryRenderer } from 'react-relay';
import environment from '../../../relay/environment';
import featuredStores from './FeaturedStores';
import SearchBox from '../../SearchBox/SearchBox';
import './Header.css';

const mapStateToProps = state => {
return {
query: state.storeService.getAllFeatured()
};
};
const Header = ({ query }) => (
<I18n>
{
(t) => (
<div className="background">
<ul className="cb-slideshow">
<li><span>Image 01</span></li>
<li><span>Image 02</span></li>
<li><span>Image 03</span></li>
</ul>
<div className="banner">
<div className="container">
<div className="banner-info">
<h2>{ t('home-page.header.title') }</h2>
<p>{ t('home-page.header.description') }</p>
</div>
<div className="banner-grads">
<QueryRenderer environment={ environment } query={ query }> render={ featuredStores }></QueryRenderer>
<div className="clearfix"></div>
<SearchBox />
</div>
</div>
</div>
</div>
)
}
</I18n>
);
export default connect(mapStateToProps)(Header);

src/components/HomePage/Header/FeaturedStores.jsx

import React from 'react';
import Spinner from 'react-spinkit';
/**
* FeaturedStores component.
*/
export default ({ error, stores }) => {
if (error) {
return <div>Error!</div>;
}
if (!stores) {
return <Spinner name="line-scale" color="blue" />;
}
return (
<div>
{
stores.map((store, key) => {
return (
<div className="col-md-4 banner-grad" key={ key }>
<div className="banner-grad-img">
<img src={ store.image } alt={ store.name } />
<h4>{ store.name }</h4>
<p>
<span className="storeDescription">{ store.description }</span>
<br /> { store.address }, { store.city }
</p>
</div>
</div>
);
})
}
</div>
);
}

src/relay/services/StoreService.jsx

import storesQuery from '../queries/StoresQuery';
import featuredStoresQuery from '../queries/FeaturedStoresQuery';
import storeQuery from '../queries/StoreQuery';
import storesByMenuItemQuery from '../queries/StoresByMenuItemQuery';

/** Limit of stores per request. */
const LIMIT = 24
/**
* class :: StoreService
*
* Service for Store types.
*/
class StoreService {
/**
* Constructor.
*/
constructor() {
this.storesQuery = storesQuery;
this.storeQuery = storeQuery;
this.featuredStoresQuery = featuredStoresQuery;
this.storesByMenuItemQuery = storesByMenuItemQuery;
this.searchFrom404 = false
this.skipCounter = 0
}
/**
* Resets the skip counter.
*/
resetSkipCounter() {
this.skipCounter = 0
}
/**
* Gets all the stores using pagination.
*
* @returns {any} GraphQL query for retrieving the stores from the API server.
*/
getAll() {
this.skipCounter += LIMIT
return this.storesQuery;
}
/**
* Gets all the featured stores.
*
* @returns {any} GraphQL query for retrieving the featured stores from the API server.
*/
getAllFeatured() {
return this.featuredStoresQuery;
}
/**
* Gets an store from the API server by its URI.
*
* @returns {any} GraphQL query for retrieving the store from the API server.
*/
getStore() {
return this.storeQuery;
}
/**
* Gets all the stores from the API server that have the given item in their menues.
*
* @param {Boolean} searchFrom404 True if the search was performed from the SearchBox component.
* @returns {any} GraphQL query for retrieving the stores from the API server.
*/
getAllByMenuItem(searchFrom404) {
this.searchFrom404 = searchFrom404 || false
return this.storesByMenuItemQuery;
}
}
/**
* Singleton implementation.
*/
export default (function () {
/** StoreService instance reference. */
let instance = null
return {
/**
* Gets a unique instance of StoreService.
*
* @returns {StoreService} A unique instance of StoreService.
*/
getInstance: function () {
if (!instance) {
instance = new StoreService()
}
return instance
}
}
})()

src/relay/query/FeaturedStoresQuery.js

import { graphql } from 'react-relay';

/**
* Gets all the featured stores.
*/
export default graphql`
query FeaturedStoresQuery {
featuredStores {
URI
name
category
address
city
image
}
}
`;

如何解决此问题并使用QueryRenderer渲染我的组件?

问题现在解决了!我删除了Redux并修复了查询渲染道具之间的额外">>"。

src/components/homePage/Header/Header.jsx

import React, { Component } from 'react';
import { I18n } from 'react-i18next';
import { QueryRenderer } from 'react-relay';
import environment from '../../../relay/environment';
import query from '../../../relay/queries/FeaturedStoresQuery';
import featuredStores from './FeaturedStores';
import SearchBox from '../../SearchBox/SearchBox';
import './Header.css';

export default class Header extends Component {
/**
* Renders the component.
* 
* @returns {string} The component's JSX code.
*/
render() {
return (
<I18n>
{
(t) => (
<div className="background">
<ul className="cb-slideshow">
<li><span>Image 01</span></li>
<li><span>Image 02</span></li>
<li><span>Image 03</span></li>
</ul>
<div className="banner">
<div className="container">
<div className="banner-info">
<h2>{ t('home-page.header.title') }</h2>
<p>{ t('home-page.header.description') }</p>
</div>
<div className="banner-grads">
<QueryRenderer environment={ environment } query={ query } render={ featuredStores } />
<div className="clearfix"></div>
<SearchBox />
</div>
</div>
</div>
</div>
)
}
</I18n>
);
}
}

我还修改了渲染函数。似乎第二个参数必须从字面上命名为道具。它不能用不同的名称命名!(存储是我的示例代码中之前使用的命名(。

src/components/HomePage/Header/FeaturedStores.jsx

import React from 'react';
import Spinner from 'react-spinkit';
/**
* FeaturedStores component.
*/
export default ({ error, props }) => {
if (error) {
return <div>Error!</div>;
}
if (!props) {
return <Spinner name="line-scale" color="blue" />;
}
return (
<div>
{
props.featuredStores.map((store, key) => {
return (
<div className="col-md-4 banner-grad" key={ key }>
<div className="banner-grad-img">
<img src={ store.image } alt={ store.name } />
<h4>{ store.name }</h4>
<p>
<span className="storeDescription">{ store.description }</span>
<br /> { store.address }, { store.city }
</p>
</div>
</div>
);
})
}
</div>
);
}

在添加 Redux 和 Relay 之前,翻译工作得很好。有人告诉我,Relay可以管理集中式状态,所以我会在尝试其他方法之前尝试删除Redux。也许两者都在一起正在折叠渲染。

QueryRenderer非常具体地要求render道具必须是:

函数类型({error, props, retry}( => React.Node。
https://relay.dev/docs/en/query-renderer.html#props

如果你看一下中继源代码,你可以看到这个render道具在这里被调用为一个函数。

当尝试将 redux 连接的组件、类组件或函数以外的任何内容传递给QueryRendererrenderprop 时,这会导致问题。

例如,react-redux中的connect函数不返回函数。它返回一个对象。因此,如果您将一个 redux 连接的组件传递给QueryRenderer,当它尝试调用此renderprop 时,您看到的"TypeError: this.props.render is not a function"错误将被抛出。尝试传递类组件会导致类似的错误,"TypeError: Cannot call a class as a function"

这个问题的一个简单解决方案是将有问题的connected/class/etc.组件包装在一个简单的渲染函数中,如下所示:

const ConnectedComponent =
connect(mapStateToProps, mapDispatchToProps)(Component);
const QueryComponent = () => (
<QueryRenderer
environment={environment}
query={query}
render={(props) => <ConnectedComponent {...props} />}
/>
);

在您的情况下,杂散的尖括号导致根本没有render道具传递给QueryRenderer,但也很容易以许多其他方式遇到此问题,如上所述。

最新更新