Super expression must either be null or a function? - reactjs

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> {...}

Related

Toggle only one element of map array with react and typescript

i'm mapping a reviews API's array and i want to show only the clicked review when i click on "read more" but at the moment is expanding all the reviews of my array, i'm using typescript and it's all new to me so i don't know how to move, how should i pass the information of the index to my state?
interface State {
reviews: Review[];
isReadMore: boolean;
}
export default class MoviePage extends Component<{}, State> {
state: State = {
reviews: [],
isReadMore: false,
};
componentDidMount() {
this.asyncAwaitFunc();
this.toggle(arguments);
}
asyncAwaitFunc = async () => {
try {
const reviewmovie = await axios.get<ReviewResponse>(
`https://api.themoviedb.org/3/movie/${this.props.match.params.id}/reviews?api_key=`
);
this.setState({
reviews: reviewmovie.data.results,
});
} catch (error) {}
};
toggle(index: any) {
this.setState({
isReadMore: !this.state.isReadMore,
});
render() {
const { isReadMore, reviews } = this.state;
return (
<>
<ReviewGrid>
{reviews.map((review, index) => (
<ReviewContent key={index}>
{this.state.isReadMore
? review.content.substring(0, 650)
: review.content}
<Button onClick={() => this.toggle(index)}>
{isReadMore ? "...read more" : " show less"}
</Button>
</ReviewContent>
))}
</ReviewGrid>
</>
);
}
}
I think that the problem is that you save isReadMore once but you need to save isReadMore for each review.
Here is an example:
interface ReviewRow {
review: Review
isReadMore: boolean
}
interface State {
reviews: ReviewRow[]
}
export default class MoviePage extends Component<{}, State> {
state: State = {
reviews: []
}
componentDidMount() {
this.asyncAwaitFunc()
}
asyncAwaitFunc = async () => {
try {
const reviewMovies = await axios.get<ReviewResponse>(
`https://api.themoviedb.org/3/movie/${this.props.match.params.id}/reviews?api_key=`
)
this.setState({
reviews: reviewMovies.data.results.map((review) => {
return { review: review, isReadMore: false }
})
})
} catch (error) {
console.log(error)
}
}
toggle(index: number) {
const { reviews } = this.state
reviews[index].isReadMore = !reviews[index].isReadMore
this.setState({ reviews })
}
render() {
const { reviews } = this.state
return (
<>
<ReviewGrid>
{reviews.map((reviewRow, index) => {
;<ReviewContent key={index}>
{ reviewRow.isReadMore ? reviewRow.review.content.substring(0, 650) : reviewRow.review..content}
<Button onClick={() => this.toggle(index)}>{reviewRow.isReadMore ? '...read more' : ' show less'}</Button>
</ReviewContent>
})}
</ReviewGrid>
</>
)
}
}
I've modified your code a bit to make this work since I don't have access to the API or the various interfaces and components you're using, but this should give you an idea. You're tracking isReadMore as a single piece of state, which is why it's toggling for every item in the array. You need to track state individually for each review. This is one of several solutions that could work, but the idea here is to take the API's response, and map it to a new set of objects which includes a new key that you'll add, isReadMore, then you can toggle this property individually for each review.
Here is the working example on CodeSandbox.
EDIT: Here is a link to a second example which does not require you to map over the results of the api call to add a new key to track isReadMore state. This approach tracks the state separately in a Map<Review, boolean> instead. An ES6 map works well here because you can use the review object itself as the key and the boolean value can track your hide/show state.
Original Solution
interface Review {
title: string;
content: string;
}
interface MyReview extends Review {
isReadMore: boolean;
}
interface State {
reviews: MyReview[];
}
export default class MoviePage extends React.Component<{}, State> {
state: State = {
reviews: []
};
componentDidMount() {
this.asyncAwaitFunc();
}
asyncAwaitFunc = () => {
try {
const reviewsFromApi: Review[] = [
{
title: "Some Movie",
content: "some movie review that's pretty long"
},
{
title: "Some Other Movie",
content: "an even longer super long movie review that's super long"
}
];
this.setState({
reviews: reviewsFromApi.map((r) => ({ ...r, isReadMore: false }))
});
} catch (error) {
console.log(error);
}
};
toggle(index: number) {
this.setState({
reviews: this.state.reviews.map((r, i) => {
if (i === index) {
return {
...r,
isReadMore: !r.isReadMore
};
}
return r;
})
});
}
render() {
const { reviews } = this.state;
return (
<>
<div>
{reviews.map((review, index) => (
<div key={index}>
<p>
{review.isReadMore
? review.content.substring(0, 10) + "..."
: review.content}
</p>
<button onClick={() => this.toggle(index)}>
{review.isReadMore ? "Read more" : " Show less"}
</button>
</div>
))}
</div>
</>
);
}
}

react input component gets store overwritten onChange

Ok, I'm new to react and mobx, and I'm experiencing some issues to manipulate the store.
When I'm typing at the input, the value gets overwritten for each char typed.
The component:
#withStore
#observer
class ConfigModel extends Component {
configModel;
constructor(props) {
super(props);
this.configModel = this.props.store.configModelStore;
}
render() {
const fieldsObj = this.configModel.modelConfig;
const fieldHelpers = this.configModel.helperModelStore.modelConfig;
const callbackOnChange = this.configModel;
const campos = merge(fieldHelpers, fieldsObj); // _.merge()
return (
<Form key={'configModelForm'}>
<>
{Object.entries(campos).map((campo) => {
if (campo[1].advanced) {
return;
}
if (campo[1].type === 'input') {
return (
<InputRender
key={campo[1].id}
field={campo[1]}
onChange={callbackOnChange.valueOnChange}
/>
);
}
})}
</>
</Form>
);
}
}
And my store define some observables (some options were omitted for simplicity, like the type evaluated at the component above):
#observable modelConfig = [{
id: 'postType',
value: '',
disabled: false,
advanced: false,
},
{
id: 'pluralName',
value: '',
disabled: false,
advanced: true,
},
...
]
And also define some actions:
#action valueOnChange = (e, {id, value}) => {
this.modelConfig.filter((config, index) => {
if (config.id === id) {
this.modelConfig[index].value = value;
console.log(this.modelConfig[index].value);
}
});
The console.log() above prints:
I truly believe that I'm forgetting some basic concept there, so can someone spot what am I doing wrong?
*EDIT:
I have another component and another store that is working correctly:
#observable name = '';
#action setName = (e) => {
this.name = e.target.value;
console.log(this.name);
}
So my question is:
Why the action that targets a specific value like this.name works fine and the action that targets a index generated value like this.modelConfig[index].value doesn't works?
The problem was at the <InputRender> component that was also receiving the #observable decorator. Just removed and it worked.
// #observer <---- REMOVED THIS
class InputRender extends Component {
render() {
const item = this.props.field;
return (
<InputField
id={item.id}
label={
<InfoLabel
label={item.label}
action={item.action}
content={item.popupContent}
/>
}
placeholder={item.placeholder}
onChange={this.props.onChange}
value={item.value}
disabled={item.disabled}
error={item.error}
throwError={item.throwError}
/>
);
}
}

Setting item in session storage when navigating to another page in React

I am trying to save a bool true when onClick when navigating to another page in React.
When the IconButton is pressed, the user navigates to /app/new and bookRide objects is being stringified and saved with status: true. When the user navigates back to the BookRide component, this status is still true.
class BookRide extends Component {
constructor(props) {
super(props);
let bookRide= JSON.parse(sessionStorage.getItem('bookRide'));
this.state = {
bookRide: bookRide? bookRide: { status: false },
}
this.handleBookRide= this.handleBookRide.bind(this);
}
handleBookRide= () => {
this.setState(() => ({
bookRide: {
...this.state.bookRide,
status: true,
}
}), function callback() {
const { bookRide} = this.state;
sessionStorage.setItem('bookRide', JSON.stringify(bookRide));
})
}
render() {
return (
<IconButton
component={Link}
to={{
pathname: `/app/new`,
}}
onClick={this.handleBookRide}
</IconButton>
)}
}
Currently, bookRide object is not being saved in sessionStorage at all.
the IconButton functionality to Link was causing an issue. Re-wrote to this:
class BookRide extends Component {
constructor(props) {
super(props);
let bookRide= JSON.parse(sessionStorage.getItem('bookRide'));
this.state = {
bookRide: bookRide? bookRide: { status: false },
}
this.handleBookRide= this.handleBookRide.bind(this);
}
handleBookRide= () => {
this.setState(() => ({
bookRide: {
...this.state.bookRide,
status: true,
}
}), function callback() {
const { bookRide} = this.state;
sessionStorage.setItem('bookRide', JSON.stringify(bookRide));
this.props.history.push (`/app/riders/new`);
})
}
render() {
return (
<IconButton
onClick={this.handleBookRide}
</IconButton>
)}
}

ReactJs redux : How to call a function from the render function if the prop value change?

I have two react components which are ProgramSearchBox and DualBox which are generic and wrapper components of predefined npm packages AutoSuggest and DualListBox respectively.
My task to achieve is Based on the value from ProgramSearchBox, I have to list the set values in the DualListBox.
So, If user select a Program from ProgramSearchBox, then I will call API by passing the ProgramId and fetch the set of result values and have to bind them in the DualListBox.
I will get the user selected ProgramID from the ProgramSearchBox as a prop in DualBox component render method.
How to dispatch an action (call a function) from render function in DualBox component by passing the ProgramId?
If I call a method from render function in DualBox, that is becoming Infinite loop!
Here is DualBox component:
//DualBox.js
class DualBox extends React.Component {
constructor() {
super();
this.state = { selected: [] };
this.onChange = this.onChange.bind(this);
this.options = [ ];
}
onChange(selected) {
selected(selected);
}
updateOptions()
{
console.log("Update Option method called :" + this.props.traineesList );
this.options = [{ value: 'luna', label: 'Moon' }, { value: 'phobos', label: 'Phobos' }];
//this.options = this.props.traineeList.map( (value,id) => )
}
render() {
const {ProgramID} = this.props; // HERE I GET ProgramID AS PROP FROM AN ANOTHER COMPONENT
const {selected} = this.state;
if(ProgramID !== "") // BASED ON THIS ProgramID VALUE, I NEED TO DISPATCH AN ACTION.
{
{this.updateProgramId(ProgramID)} // THIS IS CAUSING INFINITE LOOP
{this.updateOptions}
console.log("Program Id came to dualbox:" +ProgramID);
return <DualListBox options={this.options} selected={selected} onChange={this.onChange}
canFilter
filterCallback={(option, filterInput) => {
if (filterInput === '') {
return true;
}
return (new RegExp(filterInput, 'i')).test(option.label);
}}
filterPlaceholder="Filter..."
/>;
}
else
{
console.log("Program Id didn't come to dualbox");
return <DualListBox options={this.options} selected={selected} onChange={this.onChange}
canFilter
filterCallback={(option, filterInput) => {
if (filterInput === '') {
return true;
}
return (new RegExp(filterInput, 'i')).test(option.label);
}}
filterPlaceholder="Filter..."
/>;
}
}
}
function mapStateToProps(state, ownProps) {
return {
traineesList: state.traineesList
};
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
updateProgramId: bindActionCreators(( {ProgramID}) => dualBoxActions.getTraineesList(ProgramID), dispatch)
};
}
export default connect(mapStateToProps,mapDispatchToProps)(DualBox);
Here is the ProgramSearchBox component:
function renderSuggestion(suggestion) {
return (
<ul>
<li>{suggestion.Program}</li>
</ul>
);
}
class ProgramSearchBox extends React.Component {
constructor(props) {
super(props);
}
render() {
const { value, suggestions, onChange, onSuggestionSelected} = this.props;
const inputProps = {
placeholder: "Look Up",
value,
onChange: (event, { newValue, method }) => {
this.setState({
value: newValue
});
console.log("onChange: " + JSON.stringify(newValue) );
onChange(newValue);
}
};
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.props.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.props.onSuggestionsClearRequested}
onSuggestionSelected={
(event, { suggestion, suggestionValue, suggestionIndex, sectionIndex, method }) => {
console.log("onSuggestionSelected: " + JSON.stringify(suggestion) );
onSuggestionSelected(suggestion);
}
}
getSuggestionValue={(suggestion) => suggestion.Program}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
theme={theme}
/>
);
}
}
function mapStateToProps(state, ownProps) {
return {
suggestions: state.results
};
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onSuggestionsFetchRequested: bindActionCreators(({ value }) => searchActions.getProgramSuggestions(value), dispatch),
onSuggestionsClearRequested: bindActionCreators(() => searchActions.clearSuggestions(), dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ProgramSearchBox);
Don't call other functions in render() method. Render method is responsible only for rendering views, it can be called many times and it should be as pure as possible.
Updated answer (2019-11-21)
Use componentDidUpdate(prevProps) lifecycle function to react to prop changes.
It will look something like this:
componentDidUpdate(prevProps) {
if (this.props.ProgramID !== '' && prevProps.ProgramID !== this.props.ProgramID) {
this.updateProgramId(this.props.ProgramID)
}
}
Old answer
To do actions depending on props changing, use componentWillReceiveProps(nextProps) lifecycle function.
It will look something like this:
componentWillReceiveProps(nextProps) {
if (nextProps.ProgramID !== '' && this.props.ProgramID !== nextProps.ProgramID) {
this.updateProgramId(ProgramID)
}
}
After calling this.updateProgramId(ProgramID) props will update and render method will be called.
More info about ReactJS lifecycle:
https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops

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