我在React中有一个使用Redux和Relay的项目。客户端使用 GraphQL 连接到 API 服务器。我试图使用组件QueryRenderer并且我收到以下错误:
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/queries/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呈现我的组件?