Dynamic React-Router 1.0.0-rc3 - reactjs

Regardless of what I have read from the folks at react-router, I
prefer my app's router have dynamic data, and I have been successful
in doing it, with one caveat: I cannot loop recursively for child
routes.
Here is a working dynamic react-router:
export default class Index extends React.Component {
constructor() {
super();
this.state = {
navItems: [] };
}
componentWillMount() {
NavMenuAPI.getAll()
.then((response) => {
const data = response.data;
this.setState({ navItems: data });
});
}
fetchMenuSystem(data) {
const routes = [];
data.map(function(route) {
let routePaths = [];
let component = {};
if (route.linkTo === '/') {
const pageApp = route.title.replace(/ /g, '-').toLocaleLowerCase();
component = require('./components/pages/' + pageApp);
} else {
const pageApp = route.title.replace(/ /g, '-').toLocaleLowerCase();
component = require('./components/pages/' + pageApp);
}
if (route.paths === undefined) {
routePaths.push(route.linkTo);
} else {
routePaths = JSON.parse(JSON.stringify(route.paths));
}
routePaths.map(function(path) {
const props = { key: path, path, component };
// Static `onEnter` is defined on
// component, we should pass it to route props
// if (component.onEnter) props.onEnter = component.onEnter;
routes.push(<Route { ...props } />);
});
//////////////////////////////////////////////
// PROBLEM !!!!!!!!!!
// if (route.childNodes !== undefined) {
// this.fetchMenuSystem(route.childNodes);
// }
});
return routes;
}
fetchRoutes() {
const data = this.state.navItems;
const result = this.fetchMenuSystem(data);
return (
<Route component={ require('./components/APP') }>
{ result }
<Route path="*" component={ require('./components/pages/not-found') }/>
</Route>
);
}
render() {
if (this.state.navItems.length === 0) return <div>Loading ...</div>;
const routerProps = {
routes: this.fetchRoutes(),
history: createHistory({
queryKey: false
}),
createElement: (component, props) => {
return React.createElement(component, { ...props });
}
};
return (
<Router { ...routerProps } />
);
}
}
ReactDOM.render(<Index />, document.getElementById('reactRoot'));
As I said, this works, however, only for the first level, when I try to recursively loop through for any childNodes, I receive the error:
TypeError: Cannot read property 'fetchMenuSystem' of undefined
I tried to bind the call to the fetch function and bind the mapping, none of which worked.
I would greatly appreciate assistance on this.

OK, I solved this. The problem lied with the routes state & the 'this' keyword.
Here are the changes:
export default class Index extends React.Component {
constructor() {
super();
this.state = {
navItems: [],
routes: []
};
}
................
fetchMenuSystem(data) {
const currRoutesState = this.state.routes;
const self = this;
data.map(function(route) {
................
routePaths.map(function(path) {
................
currRoutesState.push(<Route { ...props } />);
});
if (route.childNodes !== undefined) {
self.fetchMenuSystem(route.childNodes);
}
});
return currRoutesState;
}
................
}
Final thoughts if you plan on using this:
[Re: npm install --save history]
I had to opt for 100% client side routing,
(not isomorphic/universal) so in my imports ....
import createHistory from 'history/lib/createHashHistory';
// import createBrowserHistory from 'history/lib/createBrowserHistory';

Related

component not re rendering when call action in mobx

I'm using mobx v6.
HomePage calls roomStore.fetchRooms when scrolls down to bottom, yes I use IntersectionObserver and lodash/throttle function for implement infinite scroll.
I checked roomStore.fetchRooms been called when loadMore function called, and roomStore.homeRoomList been updated.
All functions change states in Mobx stores are decorated with #action.
I wonder why my HomePage component is not re-rendered.
//RoomStore
export default class RoomStore extends BasicStore {
#observable homeRoomList: GetRoomsPayload["rooms"] | null;
constructor({root, state}: { root: RootStore, state: RoomStore}){
super({root, state});
makeObservable(this);
this.homeRoomList = state?.homeRoomList ?? null;
}
async fetchRooms(category?: string, page:number = 0){
const [error,response] = await this.api.GET<GetRoomsPayload>(`/room/${category}?page=${page}`);
if(error){
throw Error(error.error)
}
if(response && response.success){
const { data } = response
this.feedFetchHomeRooms(data.rooms);
return response.data;
}
return Promise.resolve();
}
#action.bound
feedFetchHomeRooms(rooms: GetRoomsPayload["rooms"]){
if(rooms){
if( this.homeRoomList) {
this.homeRoomList = [...this.homeRoomList, ...rooms];
}
else {
this.homeRoomList = rooms;
}
}
}
}
// HomePage Component
const HomePage: FC & HomePageInitStoreOnServer = ({}) => {
const { pathname } = useLocation();
const homeRef = useRef<HTMLUListElement>(null);
const infiniteScrollTargetRef = useRef<HTMLDivElement>(null);
const { roomStore } = useMobxStores();
const handleLoadMore = () => {
throttleFetch();
}
const throttleFetch = useCallback(throttle(() => {
roomStore.fetchRooms()
},500),[]);
useInfiniteScroll({
target: infiniteScrollTargetRef,
cb: handleLoadMore,
});
useEffect(() => {
if(!roomStore.homeRoomList){
roomStore.fetchRooms()
}
},[]);
return (
<section >
<RoomContainer ref={homeRef}>
{roomStore.homeRoomList?.map((room: any) => {
return (
<Card
room={room}
key={room.id}
/>
);
})}
</RoomContainer>
<InfiniteScroll targetRef={infiniteScrollTargetRef}/>
</section>
);
};
export default observer(HomePage);
The component (HomePage) that renders observable data needs to be wrapped into the observer.
import { observer } from 'mobx-react-lite'
const HomePage = observer(() => {
// your code of component
})
you can find more details in official docs here

React-Router: How do I add a new component and route to the onboarding steps on a wizard?

This project I am working with has an onboarding Wizard, basically some code to deal with the step by step onboarding process almost similar to what you see here:
https://medium.com/#l_e/writing-a-wizard-in-react-8dafbce6db07
except this one supposedly has a function to convert a component or step into a route:
convertStepToRoute = step => {
const Component = StepComponents[step.component || ''];
return Component
? <Route
key={step.key}
path={`${WizardLayout.pathname}/${step.url}`}
render={this.renderRouteComponent(Component)}
/>
: null;
};
StepComponents comes from import StepComponents from '../Steps'; which is a directory with all the components, they were six now seven of them that are supposed to walk the user through the onboarding process.
And its my understanding that they are pulled from the index.js file inside of Steps/ directory similar to how there would be a root reducer file in a reducers folder to export all of them, the steps component in this case like so:
import glamorous from "glamorous";
import ThemedCard from "../../ThemedCard";
import BusinessAddress from "./BusinessAddress";
import CreatePassword from "./CreatePassword";
import GetInvolved from "./GetInvolved";
import Representatives from "./Representatives";
import Topics from "./Topics";
import MemberBenefits from "./MemberBenefits";
export const StepHeader = glamorous.div({
marginBottom: 20,
marginTop: 20,
fontSize: "2rem",
color: "#757575"
});
const OnboardingCompleted = glamorous(ThemedCard)({
textAlign: "center",
boxShadow: "none !important"
});
export default {
CreatePassword,
BusinessAddress,
Completed: OnboardingCompleted,
GetInvolved,
MemberBenefits,
Topics,
Representatives
};
Well, I added mine MemberBenefits and it does not seem to work, its not rendering with its corresponding route. Where could it not be registering this new step or component?
Okay so the magic is not happening inside of Onboarding/OnBoardingWizard/index.js, its happening inside of Wizard/WizardEngine.js:
import React from "react";
import PropTypes from "prop-types";
import objectToArray from "../../../../common/utils/object-to-array";
// TODO: figure out how to use this without making children of wizard engine tied to wizardStep
// eslint-disable-next-line no-unused-vars
class WizardStep {
constructor({ component, color, order, render }, stepComponents) {
if (!component || !render) {
throw new Error("Component or render must be provided.");
}
let componentValue;
if (component) {
componentValue = this.resolveComponent(component, stepComponents);
if (!!componentValue && !React.isValidElement(componentValue)) {
throw new Error(
"wizard step expected component to be a valid react element"
);
}
} else if (render && typeof render === "function") {
throw new Error("wizard step expected render to be a function");
}
this.Component = componentValue;
this.color = color;
this.order = order;
this.render = render;
}
resolveComponent = (component, stepComponents) => {
const componentValue = component;
if (typeof component === "string") {
const componentValue = stepComponents[component];
if (!componentValue) {
throw new Error("component doesnt exist");
}
}
return componentValue;
};
}
export default class WizardEngine extends React.Component {
static propTypes = {
steps: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
initActiveIndex: PropTypes.oneOfType([PropTypes.func, PropTypes.number]),
stepComponents: PropTypes.object
};
constructor(props) {
super(props);
this.state = {
activeIndex: this.resolveInitActiveIndex(props),
steps: this.buildStepsFromConfig(props)
};
}
componentWillReceiveProps(nextProps) {
this.setState({ steps: this.buildStepsFromConfig(nextProps) });
}
resolveInitActiveIndex = props => {
const { initActiveIndex } = props;
let activeIndex = 0;
if (typeof initActiveIndex === "function") {
activeIndex = initActiveIndex(props);
}
if (typeof initActiveIndex === "number") {
activeIndex = initActiveIndex;
}
return activeIndex;
};
buildStepsFromConfig = props => {
const { steps } = props;
let stepArr = steps;
// validate stepList
if (typeof steps === "object" && !Array.isArray(steps)) {
stepArr = objectToArray(steps);
}
if (!Array.isArray(stepArr)) {
throw new Error(
`Unsupported Parameter: Wizard Engine(steps) expected either (object, array); got ${typeof stepArr}`
);
}
return stepArr;
// return stepArr.map(step => new WizardStep(step));
};
setActiveIndex = activeIndex => {
this.setState({ activeIndex });
};
goForward = () => {
this.setState(prevState => ({
activeIndex: prevState.activeIndex + 1
}));
};
goBack = () => {
this.setState(prevState => ({
activeIndex: prevState.activeIndex - 1
}));
};
render() {
const { children } = this.props;
const childProps = {
...this.state,
setActiveIndex: this.setActiveIndex,
goForward: this.goForward,
goBack: this.goBack,
currentStep: this.state.steps[this.state.activeIndex]
};
if (Array.isArray(children)) {
return (
<div>
{children.map((child, i) => {
if (typeof child === "function") {
return child(childProps);
}
childProps.key = `${child.type.name}_${i}`;
return React.cloneElement(child, childProps);
})}
</div>
);
}
if (typeof children === "function") {
return children(childProps);
}
return children;
}
}
I think the first method load the element only when it needed.
The second method load all methods everytime. Why to load Home when you are in /Products?
The path URL is being mapped on the backend utilizing the Entity Framework similar to the setup you can view here in this documentation:
https://dzone.com/articles/aspnet-core-crud-with-reactjs-and-entity-framework
except it is being done in Express.
So it's not using React-Router in the traditional sense where Express allows it to control the whole mapping route paths to components, but instead the path to the onboarding component is being mapped here inside the Express src/app-server/apiConfig.js like so:
"get-involved-onboarding": {
title: "Get Involved",
url: "/account/onboarding/get-involved",
icon: "explore",
component: "GetInvolved",
progress: {
stepType: "GetInvolved",
hasCompleted: true
}
},

How to stub document method with sinon - React

import React, { PropTypes, Component } from 'react';
import classNames from 'classnames/bind';
import { get, includes } from 'lodash';
import { Link } from 'react-router';
import * as styles from '../CAMNavPanel.css';
const cx = classNames.bind(styles);
class CAMNavPanelListItem extends Component {
static propTypes = {
navData: PropTypes.shape({
title: PropTypes.string,
isRedirect: PropTypes.bool,
url: PropTypes.string,
}).isRequired,
location: PropTypes.shape({ pathname: PropTypes.string.isRequired,
query: PropTypes.objectOf(PropTypes.object).isRequired,
search: PropTypes.string.isRequired,
}).isRequired,
};
constructor() {
super();
this.state = { currentView: '' };
this.getClasses.bind(this);
}
// in case of url being manually set, figure out correct tab to highlight
componentWillMount() {
this.changeLocation();
}
// give correct tab the 'active' class
getClasses(navData) {
const { location } = this.props;
const activeClass = 'active';
let isContainedInOtherUrls = false;
if (get(navData, 'otherUrls') && includes(navData.otherUrls, location.pathname)) {
isContainedInOtherUrls = true;
}
if ((this.state.currentView === navData.url) || isContainedInOtherUrls) {
return activeClass;
}
return '';
}
getActiveClass(e, navData) {
const elements = document.getElementsByClassName('CAMNavPanel-rewardsMenu')[0].getElementsByTagName('li');
for (let i = 0; i < elements.length; i += 1) {
elements[i].className = '';
}
this.setState({ currentView: navData.url }, () => {
if (get(navData, 'scrollIntoView')) {
document.getElementsByClassName(navData.scrollIntoView)[0].scrollIntoView();
}
});
}
// update state based on the URL
changeLocation() {
const { location } = this.props;
const currentView = location.pathname;
this.setState({ currentView });
}
render() {
const { navData } = this.props;
let target = '';
if (navData.isExternalLink) {
target = '_blank';
}
return (
<li className={cx(this.getClasses(navData))} key={navData.title}>
{ navData.isRedirect ? <a href={navData.url} target={target}>
{navData.title}</a> :
<Link to={navData.url} onClick={e => this.getActiveClass(e, navData)}>{navData.title}</Link> }
</li>
);
}
}
export default CAMNavPanelListItem;
Test case:
describe('CAMNavPanelListItem with isRedirect false plus highlight li', () => {
let wrapper;
const navData = {
title: 'My Orders',
isRedirect: false,
isExternalLink: false,
url: '/orders',
};
const location = {
pathname: '/orders',
};
beforeEach(() => {
documentObj = sinon.stub(document, 'getElementsByClassName');
const li = {
getElementsByTagName: sinon.stub(),
};
documentObj.withArgs('CAMNavPanel-rewardsMenu').returns([li]);
wrapper = shallow(
<CAMNavPanelListItem
navData={navData}
location={location}
/>,
);
wrapper.setState({ currentView: navData.url });
});
it('should render CAMNavPanelListItem with Link as well', () => {
expect(wrapper.find('li')).to.have.length(1);
expect(wrapper.find('li').hasClass('active')).to.equal(true);
expect(wrapper.find('Link')).to.have.length(1);
});
it('should click and activate activeClass', () => {
wrapper.find('Link').simulate('click', { button: 0 });
});
afterEach(() => {
wrapper.unmount();
documentObj.restore();
});
});
Errors I am getting:
const elements = document.getElementsByClassName('CAMNavPanel-rewardsMenu')[0].getElementsByTagName('li');
console.log('Elements of getElementsByTagName', elements);
The elements I am getting as undefined.
Please help. How do I stub after the click of the Link element.
You might want to look into something like jsdom to mock out an entire DOM instead of manually mocking a couple of the DOM functions. Then, you wouldn't have to implement the document.getElementsByClassName() function and it would make the test suite a bit more robust to changes in the component's implementation. You would just test for certain elements using functions on the jsdom itself.
You could also try mocha-jsdom to automatically setup and teardown the test DOM for each describe() block.
From what it seems, you made a stub to getElementsByTagName, but did not define to return anything, so you are getting undefined.
Add the following code:
const li = {
getElementsByTagName: sinon.stub(),
};
// add the code below after the above code lines.
const elements = [];
li.getElementsByTagName.withArgs('li').returns(elements);
This is what worked for me.
global.window.document.querySelectorAll = sinon.stub();
global.window.document.getElementsByClassName = sinon.stub();
const scrollIntoView = sinon.stub();
global.window.document.querySelectorAll.returns([{ className: '' }]);
global.window.document.getElementsByClassName.returns([{
scrollIntoView,
}]);
and I changed my original code to:
const elements = document.querySelectorAll('.CAMNavPanel-rewardsMenu li');
Used the global window object

Console.log in ReactJS keeps running

I'm importing a child component and rendering it in my app.js file. The component has a console.log for debugging but it keeps running the log, seemingly without end. Worried something might be wrong, new to ReactJS and wondering if this is a common issue and how to resolve it.
App.js:
import React, { Component } from 'react';
import {BrowserRouter as Router, Link} from 'react-router-dom';
import './App.css';
import axios from 'axios'
import Header from './components/header';
import Page from './components/page';
class App extends Component {
constructor(props) {
super(props);
this.state = {
title: 'John Doe',
nav: {},
currentPage: "",
pageContent: "",
pageTitle: "",
pageTemplate: "",
pageId: 0,
pageCustomMeta: {},
archiveData: []
}
}
getMainMenu(){
axios.get('http://admin.sitedata.co/menus/5')
.then((response) => {
this.setState({nav:response.data});
})
.catch((error) => {
console.log(error);
});
}
isHome(){
//console.log(document.location.pathname);
if(document.location.pathname === "/") {
document.body.classList.add('home');
} else {
document.body.classList.remove('home');
}
}
componentDidMount(){
/*
* get current page content
* get the main menu
* allow pageChange function to be ran
* allow isHome to be ran
*/
var slug = "";
if(document.location.pathname === "/") {
slug = "home";
} else {
slug = document.location.pathname.substr(1);
}
this.getPageData(slug);
this.getMainMenu();
this.pageChange = this.pageChange.bind(this);
this.isHome = this.isHome.bind(this);
this.triggerMenu = this.triggerMenu.bind(this);
this.triggerHire = this.triggerHire.bind(this);
this.navigate = this.navigate.bind(this);
this.setArchiveData = this.setArchiveData.bind(this);
this.resetArchiveData = this.resetArchiveData.bind(this);
this.madeChange = this.madeChange.bind(this);
//document.getElementById('loadingOverlay').classList.add('remove');
}
getPageData(slug){
console.log(this.state);
axios.get('http://admin.sitedata.co/pages?slug='+slug)
.then((response) => {
console.log(response.data);
this.setState({
pageContent:response.data[0].content.rendered,
currentPage:slug,
pageTitle:response.data[0].title.rendered,
pageTemplate:response.data[0].template,
pageId:response.data[0].id,
pageCustomMeta:response.data[0].post_meta,
archiveData:[]
},function(){
console.log(this.state);
/*
* set the page title
* check if the page is at home
* get page custom meta
*/
document.title = this.state.pageTitle;
this.isHome();
});
})
.catch((error) => {
console.log(error);
});
}
pageChange(e){
var slug = e.target.getAttribute('data-link');
var classes = e.target.classList.contains('trigger-hire');
if(classes){
this.triggerHire();
e.preventDefault();
} else {
this.getPageData(slug);
}
}
setArchiveData(archives) {
this.setState({archiveData:archives});
}
resetArchiveData() {
}
navigate (event) {
event.preventDefault()
console.log(event.target.tagName);
if (event.target.tagName === 'A') {
this.props.router.push(event.target.getAttribute('href'));
console.log('boom');
}
event.preventDefault();
}
triggerMenu(e){
var menuOverlay = document.getElementById('menuOverlay');
if(menuOverlay.classList.contains('active')){
menuOverlay.classList.remove('active');
} else {
menuOverlay.classList.add('active');
}
}
triggerHire(e){
var hireOverlay = document.getElementById('hireOverlay');
if(hireOverlay.classList.contains('active')){
hireOverlay.classList.remove('active');
} else {
hireOverlay.classList.add('active');
}
e.stopPropagation();
e.preventDefault();
}
madeChange(){
alert('changed');
}
render() {
return (
<div className="App">
<Header nav={this.state.nav} pageChange={this.pageChange} triggerMenu={this.triggerMenu} triggerHire={this.triggerHire}/>
<Page madeChange={this.madeChange}
currentPage={this.state.currentPage}
nav={this.state.nav}
pageChange={this.pageChange}
isHome={this.isHome}
pageContent={this.state.pageContent}
pageTitle={this.state.pageTitle}
pageTemplate={this.state.pageTemplate}
pageId={this.state.pageId}
pageCustomMeta={this.state.pageCustomMeta}
archiveData={this.state.archiveData}
triggerHire={this.triggerHire}
navigate={this.navigate}
setArchiveData={this.setArchiveData}
/>
</div>
);
}
}
export default App;
Page.js
import React, { Component } from 'react';
import {BrowserRouter as Router, Link} from 'react-router-dom';
import axios from 'axios'
class Page extends Component {
render() {
if(this.props.currentPage){
var currentPage = this.props.currentPage;
var pageChange = this.props.pageChange;
var customMeta = this.props.pageCustomMeta;
var pageTempalate = this.props.pageTemplate.substr(0,this.props.pageTemplate.length-4);
var pageTitle = this.props.pageTitle;
var newTitle = <h1><span><i>{pageTitle}</i></span></h1>;
var isArchive = "";
var archiveName = "";
var firstSpace = pageTitle.indexOf(' ');
if(firstSpace > -1){
var firstWord = pageTitle.substr(0, firstSpace);
var titleLast = pageTitle.substr(firstSpace);
newTitle = <h1><span><i>{firstWord}</i>{titleLast}</span></h1>
}
if(currentPage === "home"){
if(this.props.nav.items){
var navData = this.props.nav;
var navHomeItems = navData.items.map(function(navItem){
return <li key={navItem.id}><Link to={'/'+navItem.object_slug} className={navItem.classes} onClick={pageChange} data-link={navItem.object_slug}>{navItem.title}</Link></li>;
});
}
}
document.title = this.props.pageTitle;
var isArchive = customMeta.isArchive;
var archiveName = customMeta.archiveName
var worksArchive = "";
if(customMeta.isArchive && customMeta.isArchive == "true"){
if(customMeta.archiveName) {
axios.get('http://admin.sitedata.co/'+customMeta.archiveName)
.then((response) => {
var archivePages = response.data;
console.log(archivePages);
if(archiveName == "works"){
worksArchive = archivePages.map(function(work){
//console.log(worksArchive);
return <Link key={work.id} className="work-item" to="/" ><img src={work.post_meta.targetimg} /></Link>;
});
this.props.setArchiveData(worksArchive);
}
})
.catch((error) => {
console.log(error);
});
}
}
if(customMeta.pageColor){
document.body.classList.add(customMeta.pageColor);
}
if(customMeta.bgimg){
document.body.setAttribute('style', "background-image:url('"+customMeta.bgimg+"');");
}
}
return (
<Router onEnter={this.props.madeChange}>
<div className="container">
<div className={(pageTempalate !== "") ? pageTempalate : ''}>
{newTitle}
<div dangerouslySetInnerHTML={{__html:this.props.pageContent}}></div>
{this.props.archiveData}
</div>
</div>
</Router>
);
}
}
export default Page;
Distilling your code down to the essential, it looks like this:
class Page extends Component {
render() {
axios.get(url).then(response=> {
this.setState({archiveData: response.data})
})
return (
<div className="container">
{this.state.archiveData}
</div>
)
}
}
(You are doing it differently with a callback that causes the parents to send new props, but the effect is the same).
You should be able to see problem now: the render method causes a delayed change to state (or props) which in react triggers a new render. So you now have an infinite loop, just delayed by the time taken for an ajax request.
To fix this, you need to remove the ajax request from the render method. In your case, it should probably be in the parent App component.

Recursive data & components, later fetches throwing an error

First off my graphql data model:
type Human {
id: !String,
name: !String,
children: [Human]
}
The only route (relay route config) I'm atm using:
class extends Relay.Route {
static queries = {
human: () => Relay.QL`query RootQuery { viewer }`
};
static routeName = 'AppHomeRoute';
}
The list component:
class HumanList extends Component {
render() {
let {children} = this.props.human;
let subListsHTML = human ? children.map(child => (
<HumanListItem key={child.id} human={child}/>
)) : '';
return <ul>{subListsHTML}</ul>;
}
}
export default Relay.createContainer(HumanList, {
fragments: {
human: () => Relay.QL`
fragment on Human {
children {
id,
${HumanListItem.getFragment('human')}
}
}
`
}
});
The list item component:
class HumanListItem extends Component {
state = {expanded: false};
render() {
let {human} = this.props;
let sublistHTML = '';
if (this.state.expanded) {
sublistHTML = <ul><HumanList human={human}/></ul>;
}
return (
<li>
<div onClick={this.onClickHead.bind(this)}>{human.name}</div>
{sublistHTML}
</li>
);
}
onClickHead() {
this.props.relay.setVariables({expanded: true});
this.setState({expanded: true});
}
}
HumanListItem.defaultProps = {viewer: {}};
export default Relay.createContainer(HumanListItem, {
initialVariables: {
expanded: false
},
fragments: {
human: (variables) => Relay.QL`
fragment on Human {
name,
${HumanList.getFragment('human').if(variables.expanded)}
}
`
}
});
Which runs fine for the root list. But as soon as I click on a ListItem and it is expanded, I get the following error:
Warning: RelayContainer: Expected prop 'human' supplied 'HumanList' to be data fetched by Relay. This is likely an error unless you are purposely passing in mock data that conforms to the shape of this component's fragment.
I can't make much sense of it, since the data I'm passing is not mocked but directly fetched by Relay as can be seen in the HumanList comp.
The error indicates that the <HumanList> is being rendered before its data is ready.
class HumanListItem extends Component {
onClickHead() {
this.props.relay.setVariables({expanded: true});
this.setState({expanded: true}); // <-- this causes the component to re-render before data is ready
}
Rather than using state, you can instead look at the current value of the variables:
class HumanListItem extends Component {
// no need for `state.expanded`
render() {
let {human} = this.props;
let sublistHTML = '';
if (this.props.relay.variables.expanded) {
// `variables` are the *currently fetched* data
// if `variables.expanded` is true, expanded data is fetched
sublistHTML = <ul><HumanList human={human}/></ul>;
}
return (
<li>
<div onClick={this.onClickHead.bind(this)}>{human.name}</div>
{sublistHTML}
</li>
);
}
onClickHead() {
this.props.relay.setVariables({expanded: true});
// no need for `setState()`
}
}
HumanListItem.defaultProps = {viewer: {}};
export default Relay.createContainer(HumanListItem, {
initialVariables: {
expanded: false
},
fragments: {
human: (variables) => Relay.QL`
fragment on Human {
name,
${HumanList.getFragment('human').if(variables.expanded)}
}
`
}
});

Resources