Recursive data & components, later fetches throwing an error - reactjs

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)}
}
`
}
});

Related

how to create refs for content that gets created later

I have a component that fetches data from an api upon user input. This data then gets rendered onto the screen as <li/> tags. I want those <li/> tags to have a ref.
I tried creating an object of refs that I create after the data is fetched:
this.singleRefs = data.reduce((acc, value) => {
acc[value.id] = React.createRef();
return acc;
}, {});
and then later assign these refs to the <li/> tag: <li ref={this.singleRefs[element.id]}>
but when I print them out I always have {current:null} Here is a demo
what am I doing wrong?
With dynamic ref data, I'd propose that you should use callback refs.
import React from "react";
import "./styles.css";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
this.singleRefs = {};
}
componentDidMount() {
const data = [
{ value: "val1", id: 1 },
{ value: "val2", id: 2 },
{ value: "val3", id: 3 }
];
this.myFunc(data);
//you don't need this anymore
// this.singleRefs = data.reduce((acc, value) => {
// acc[value.id] = React.createRef();
// return acc;
// }, {});
}
myFunc = async (data) => {
await sleep(3000);
this.setState({ data });
};
renderContent() {
return this.state.data.map(
function (element, index) {
return (
<li key={index} ref={(node) => (this.singleRefs[element.id] = node)}>
{element.value}
</li>
);
}.bind(this)
);
}
render() {
console.log(this.singleRefs);
return <ul>{this.renderContent()}</ul>;
}
}
Sandbox

Call a function of one of a list of children components of the parent component using reference

For my website I want to include a feature that helps users randomly click a link programatically. The event happens in the parent component called StreamingPlaza, and its has a list of children components called StreamingCard, each containing a streaming link. Below is my code:
StreamingPlaza
class StreamingPlaza extends Component {
state = {
......
}
roomclicks = [];
componentDidMount() {
//Approach 1//
this.roomclicks[0].current.handleClick();
//Approach 2//
this.roomclicks[0].props.click = true;
......
}
setRef = (ref) => {
this.roomclicks.push(ref);
}
renderRoom = (room) => {
return <StreamingCard info={room} ref={this.setRef} click={false}></StreamingCard>;
}
render () {
const rooms = this.props.rooms;
return (
{ rooms && rooms.map (room => {
return this.renderRoom(room);
})
}
);
}
StreamingCard
class StreamingCard extends Component {
constructor(props){
super(props);
this.state = {
......
}
}
handleClick = () => {
document.getElementById("link").click();
}
render() {
return (
✔️ Streaming Link: <a id="link" href=......></a>
);
}
Regarding Approach 1, the console reported the error Cannot read property handClick of undefined. After I removed "current", it said that this.roomclicks[0].handleClick is not a function. Regarding Approach 2, I was not able to modify the props in this way, as the console reported that "click" is read-only.
Approach 1 is basically how its need to be done, but with React API.
See React.createRef
class StreamingPlaza extends Component {
roomclicks = React.createRef([]);
componentDidMount() {
// 0 is room.id
this.roomclicks.current[0].handleClick();
}
renderRoom = (room) => {
return (
<StreamingCard
info={room}
ref={(ref) => (this.roomclicks.current[room.id] = ref)}
click={false}
></StreamingCard>
);
};
render() {
const rooms = this.props.rooms;
return rooms.map((room) => {
return this.renderRoom(room);
});
}
}

Super expression must either be null or a function?

The component must be part of the actions column and be rendered for the "workflow" type
The component should be able to render only a button, which when clicked starts the workflow configured in the action, OR a dropdown with different options which when clicked start the workflow with the clicked option as the workflow arguments
The component should use the connectWorkflow decorator, which adds different props for interacting with the workflows API, e.g. startFlow, resumeFlow. The functions and their arguments can be seen in the WorkflowManager class
When the user clicks the button or an option the component should call the startFlow function from the props, with the workflowPath configured in the action
The component should be able to pass input data to the workflow, that is retrieved from the specific table row data. It should be able to accept an option in the action definition in the ListPage columns prop, that is an Object which will be passed as the input data to the startFlow function. Before being passed any key or value from this object should be checked if there are some values in them that should be replaced with the table row's data
type Props = {
workflowPath: string;
executionId: string,
data: Object,
actionHandlers: {
[string]: {
async: boolean,
func: (data: { executionId: string, [string]: any }, context: Object) => any,
},
},
startFlow: Function,
resumeFlow: Function,
};
type State = {
workflowCode: string,
executionId: string,
loading: boolean,
}
#connectWorkflow
class Workflow extends React.Component<Props, State> {
static defaultProps = {
executionId: '',
data: {},
actionHandlers: {},
startFlow: () => undefined,
resumeFlow: () => undefined,
};
state = {
workflowCode: '',
executionId: '',
loading: true,
};
componentDidMount() {
const {
workflowPath, executionId, startFlow, resumeFlow, data, actionHandlers,
} = this.props;
if (executionId) {
resumeFlow(executionId, data, actionHandlers).then(({ id: execId, workflow_name: workflowCode }) => {
this.setState({ executionId: execId, workflowCode, loading: false });
});
} else {
startFlow(workflowPath, data, actionHandlers).then(({ id: execId, workflow_name: workflowCode }) => {
this.setState({ executionId: execId, workflowCode, loading: false });
});
}
}
componentDidUpdate(prevProps: Props) {
const {
workflowPath, executionId, startFlow, resumeFlow, data, actionHandlers,
} = this.props;
if (prevProps.workflowPath !== workflowPath) {
if (executionId) {
resumeFlow(executionId, data, actionHandlers).then(({ id: execId, workflow_name: workflowCode }) => {
this.setState({ executionId: execId, workflowCode, loading: false });
});
} else {
startFlow(workflowPath, data, actionHandlers).then(({ id: execId, workflow_name: workflowCode }) => {
this.setState({ executionId: execId, workflowCode, loading: false });
});
}
}
}
render() {
const { executionId: executionIdProps } = this.props;
const { executionId, loading, workflowCode } = this.state;
// TODO: i18n
return (
<React.Fragment>
<WorkflowForm
workflowCode={workflowCode}
executionId={executionIdProps || executionId}
/>
{loading && (
<Layer margin="medium" plain>
<Box>
<Text>Loading</Text>
</Box>
</Layer>
)}
</React.Fragment>
);
}
}
export default Workflow;
Then I have error here: Super expression must either be null or a function
// #flow
import * as React from 'react';
import { Box, Button } from 'grommet';
import { Launch } from 'grommet-icons';
import connectWorkflow from '../../../../../../../../src/components/workflows/connectWorkflow';
type Props = {
startFlow: Function,
}
#connectWorkflow
class WorkflowComponent extends React.ComponentType<Props> {
static defaultProps = {
startFlow: () => {
},
};
handleStart = () => {
this.props.startFlow();
};
render() {
return (
<Box>
<Button
label="Star Flow"
position="right"
icon={<Launch />}
onClick={this.handleStart}
/>
</Box>
);
}
}
export default WorkflowComponent;
The error means that parent class is not valid class but something else.
React.ComponentType is a type, not a class. It doesn't exist at run time, another class cannot extend it. WorkflowComponent should extend React.Component. With types it likely should be:
class WorkflowComponent extends React.Component<Props> {...}

How to access state values by index in React

I'm struggling to access values inside a state in React using axios and my code is as follows:
import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
class App extends React.Component {
state = {
moviedata:null
}
getMovies(){
axios.get("http://127.0.0.1:8000/api/v1/movies/")
.then(moviedata => {
this.setState({
moviedata: moviedata.data
});
})
.then(x => { console.log(this.state.moviedata)});
}
componentDidMount(){
this.getMovies();
}
render () {
return <h1>Movie Examples include </h1>
}
}
ReactDOM.render(<App />, document.getElementById('react-app'));
The console.log looks like this:
0: {title: "Terminator 2: Judgement Day", plot: "Rise of the machines.", year: 1991}
1: {title: "The Italian Job", plot: "A comic hinging on a traffic jam", year: 1969}
How can I include the title of the first entry, i.e. 'Terminator 2: Judgement Day', inside the h1 tag, after the word 'include'?
I tried:
render () {
return <h1>Movie Examples include {this.state.moviedata[0].title}</h1>
}
and got an error TypeError: Cannot read property '0' of null
moviedata in your component state is initially null, so trying to access [0] from that will give rise to your error.
You could e.g. return early from the render method until moviedata has been set.
Example
class App extends React.Component {
// ...
render() {
const { moviedata } = this.state;
if (moviedata === null) {
return null;
}
return <h1>Movie Examples include {moviedata[0].title}</h1>;
}
}
You have to account for the fact that the Axios request is asynchronous, so the component may render before the data are loaded. For example:
render () {
const data = this.state.moviedata;
return <h1>Movie Examples include {data ? data[0].title : ""}</h1>
}
First, you have to "know" that the component is in a "loading" state. Without that, your state data is undefined (Still loading)
Here's how to do it:
class App extends React.Component {
state = {
moviedata:null,
isLoading: true
}
getMovies(){
axios.get("http://127.0.0.1:8000/api/v1/movies/")
.then(moviedata => {
this.setState({
moviedata: moviedata.data,
isLoading: false
});
})
.then(x => { console.log(this.state.moviedata)});
}
componentDidMount(){
this.getMovies();
}
render () {
if (this.state.isLoading) {
return <h1>Please wait...</h1>
}
// show only the first movie
return <h1>Movie #1 {this.state.moviedata[0].title}</h1>;
// show all the movies
return (
<>
{this.state.moviedata.map((m, idx) => <h1 key={idx}>Movie: {m.title}</h1>}
</>);
}
}

Undefined props in componentDidMount

This is starting to get really frustrating. Basically, I cannot access props in my subcomponents. if I try to render them directly using this.props- it works, but if I need to do additional processes with them, or save them into state, I get undefined props all the time. I have a parent component, which looks something like this:
import React from 'react';
import Title from './EventSubComponents/Title';
import SessionInfo from './EventSubComponents/SessionInfo';
import SessionTime from './EventSubComponents/SessionTime';
import Location from './EventSubComponents/Location';
import Subscribers from './EventSubComponents/Subscribers';
class EventNode extends React.Component {
constructor(props) {
super(props);
this.state = {
'event': [],
}
}
componentDidMount() {
this.getEvent(this.props.location.selectedEventId);
}
getEvent(eventId) {
fetch('/api/v.1.0/event/' + eventId, {mode: 'no-cors'})
.then(function(response) {
if(!response.ok) {
console.log('Failed to get single event.');
return;
}
return response.json();
})
.then((data) => {
if (!data) {
return;
}
this.setState({
'event': data
})
});
}
render() {
return(
<div className="event-wrapper">
<Title
title = { this.state.event.title }
date = { this.state.event.start }
/>
<SessionInfo
distance = { this.state.event.distance }
type = { this.state.event.type }
/>
<SessionTime
start = { this.state.event.start }
end = { this.state.event.end }
/>
<Location location = { this.state.event.start_location }/>
<Subscribers
subscribers = { this.state.event.subscribers }
eventId = { this.state.event._id }
/>
</div>
);
}
}
export default EventNode;
And my sub-component SessionTime, which looks like this:
import React from 'react';
import moment from 'moment';
class Title extends React.Component {
constructor(props) {
super(props);
this.state = {
'title': '',
'date': '',
}
}
componentDidMount() {
console.log(this.props.title);
console.log(this.props.date);
// undefined both props.
this.convertToTitleDate(this.props.date);
this.setState({
'title': this.props.title
})
}
convertToTitleDate(date) {
var newDate = moment(date).format('dddd, Do MMMM')
this.setState({
'date': newDate,
});
}
render() {
return (
<div className="event-title-wrapper">
<h1> { this.state.title } </h1>
<div className="event-title-date"> { this.state.date } </div>
</div>
);
}
}
export default Title;
Could anyone explain, why both this.props.date and this.props.title are undefined in my componentDidMount function? I have couple more components in my EventNode and I have the same problems in them as well.
Changing componentDidMount to componentWillMount does not help. I am fairly certain I have problems in my parent EventNode component, but I cannot figure out where. Inside EventNode render() all the state variables are defined.
You initialize event to an empty array and pass down this.state.event.start and this.state.event.end to SessionTime, which will both be undefined on first render since event has not been loaded yet and there are no start and end properties on the array.
You could instead e.g. set event to null initially, and return null from the render method until the event has been loaded.
Example
class EventNode extends React.Component {
state = {
event: null
};
// ...
render() {
const { event } = this.state;
if (event === null) {
return null;
}
return (
<div className="event-wrapper">
<Title title={event.title} date={event.start} />
<SessionInfo distance={event.distance} type={event.type} />
<SessionTime start={event.start} end={event.end} />
<Location location={event.start_location} />
<Subscribers
subscribers={event.subscribers}
eventId={this.state.event._id}
/>
</div>
);
}
}

Resources