ReactJS | Cannot update during an existing state transition - javascript

I am trying to add Delete Functionality, to my React Application. So, I created a Delete Model Component. And I am using it in my main page.
Main-Page Component:
import IUser from '../../dto/IUser';
import DeleteUser from '../../components/DeleteUser';
import { listUsers, getUser, deleteUser } from '../../config/service';
interface UserDetailsProps extends RouteComponentProps<RouteUserInfo> {
notify(options: object): any;
actualValue: string;
callBack: any;
label: string;
}
interface RouteUserInfo {
username: string;
}
export interface IState {
errorMessage: LensesHttpResponseObj | null;
isUserDeleteModalOpen: boolean;
isLoading: boolean;
user: IUser | null;
}
const UserToolTip = (props: any): JSX.Element => (
<LensesTooltip id="isActive" place="right" {...props} />
);
export class UserDetailsPage extends Component<UserDetailsProps, IState> {
hasBeenMounted = false;
state: IState = {
isUserDeleteModalOpen: false,
errorMessage: null,
isLoading: false,
user: null
};
componentDidMount(): any {
this.hasBeenMounted = true;
this.onFetchData();
}
componentWillUnmount(): void {
this.hasBeenMounted = false;
}
getUserUsername = (): string => {
const { match } = this.props;
return match.params.username;
};
onFetchData = () => {
this.setState({
isLoading: true
});
return this.onFetchUser();
};
onFetchUser = () =>
getUser(this.getUserUsername())
.then(username => {
if (this.hasBeenMounted && typeof username !== 'undefined') {
this.setState({
isLoading: false,
user: username.data
});
}
})
.catch((errorResponse: HttpResponseObj | null = null) => {
if (this.hasBeenMounted) {
this.setState({
isLoading: false,
user: null,
errorMessage: errorResponse
});
}
});
openUserDeleteModal = () => {
this.setState(prevState => ({
...prevState,
isUserDeleteModalOpen: true
}));
};
closeUserDeleteModal = () => {
this.setState(prevState => ({
...prevState,
isUserDeleteModalOpen: false
}));
};
// Dropdown Render Method:
<Item onClick={this.openUserDeleteModal()}> // The error appears when I add the onClik
<Icon icon="trash-o" className=" pl-0 py-2 col-1" />
<span className="col pr-0 mr-0">Delete User</span>
</Item>
Of course I call the dropdown render Method inside the main render(), along with the render method for the Delete Component:
renderUserDeleteModal = (): JSX.Element | null | void => {
const { isUserDeleteModalOpen, user } = this.state;
if (!user || !user.username) {
return null;
}
return (
<DeleteUser
isModalOpen={isUserDeleteModalOpen}
user={user}
onSuccess={this.closeDeleteModalSuccess}
onCloseModal={this.closeUserDeleteModal}
/>
);
};
But I get this ERROR: warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state.
I am not sure what am I doing wrong here. To me, it seems legit. Can you explain what am I doing wrong. Thank you!!

you are making call to openUserDeleteModal onClick={this.openUserDeleteModal()} which is causing update of state while rendering the component try the following :
<Item onClick={this.openUserDeleteModal}>
onClik
<Icon icon="trash-o" className=" pl-0 py-2 col-1" />
<span className="col pr-0 mr-0">Delete User</span>
</Item>

You need not invoke the callback to your onClick as that will end up being immediately called upon render.
Remove the parenthesis following onClick={openUserDelete()}.
Your openUserDelete is being called straight away (upon render) and changes the state Object.
changing the state causes a re-render and you can imagine how this would get out of hand...
render > change > render > change...etc

Related

not passing state correctly in react typescript

I am trying to toggle a modal from separate components. the first most common component is my app.tsx so i set the state in that file.
type TokenUpdateType = {
sessionToken: string | undefined | null,
createActive: boolean
}
export default class App extends Component<{}, TokenUpdateType> {
constructor(props: TokenUpdateType) {
super(props)
this.state = {
sessionToken: undefined,
createActive: false
}
...
toggleModal = () => {
this.setState({createActive: !this.state.createActive})
}
return
<Home isOpen={this.state.createActive} toggleModal={this.toggleModal} />
my home component takes these props and passes again to another component
type AuthProps = {
isOpen: boolean
toggleModal: () => void
...
}
const Home = (props: AuthProps) => {
return(
<>
<Sidebar sessionToken={props.sessionToken} toggleModal={props.toggleModal}
<ChannelEntryModalDisplay sessionToken={props.sessionToken} isOpen={props.isOpen} toggleModal={props.toggleModal}/>
</>
)
}
isOpen gets passes to my modal component and is used in this component
type AuthProps = {
isOpen: boolean
toggleModal: () => void
...
}
const ChannelEntryModalDisplay = (props: AuthProps) => {
return(
<div>
<Modal show={props.isOpen}>
<ChannelEntry sessionToken={props.sessionToken}/>
<Button className='button' type='button' outline onClick={props.toggleModal}>close</Button>
</Modal>
</div>
)
}
my modal is not showing even when i set createactive to true. i believe i may be passing props incorrectly but im not sure what i am doing incorrectly. i appreciate any feedback.
try to create a new state from the props:
const [createActive, setCreateActive] = useState<boolean>()
constructor(props: TokenUpdateType)
{
super(props)
setCreateActive(props.createActive)
}
useEffect(() => {
setCreateActive(props.createActive) // update the state when props changes
}, [props])
...
toggleModal = () => {
this.setCreateActive(!createActive)
}
<Home isOpen={createActive} toggleModal={this.toggleModal} />

How to remove data from API with HTTP Request [React TS]

so I've been working on an admin control panel for a page that displays a list of cards that once clicked, redirect the user to either a video ora text-based link. I must also add a CRUD feature that allows me to edit / remove posts from the API provided.
The API is locally hosted, so requesting "localhost:3000/videos/{Id}" will load up the object with the following fields: "id", "url", "title" and "thumbnail"
I have a class that is used for making the card, called HelpCard.tsx The code is as follows:
import React, { Component } from "react";
import "../help/HelpCard.css";
import "../help/HelpList";
import spinner from "../help/spinner.gif";
import { string, number } from "prop-types";
import { Link } from "react-router-dom";
interface Props {
url: string;
title: string;
thumbnail: string;
}
interface State {
title: string;
thumbnail: string;
id: string;
imageLoading?: boolean;
tooManyRequests?: boolean;
}
export default class HelpCard extends Component<Props, State> {
state = {
id: "",
title: "",
imageLoading: true,
tooManyRequests: false,
thumbnail: "",
deleteProduct: true
};
componentDidMount() {
const { url, title, thumbnail } = this.props;
const id = url.split("/")[url.split("/").length - 2];
this.setState({
id,
title,
thumbnail,
imageLoading: true,
tooManyRequests: false
});
}
render() {
const isThumbnail = this.state.thumbnail;
const adminhelpcard = this.state;
return (
<div>
{isThumbnail ? (
<div className="horizontalCard">
<div className="innerCard">
<div className="leftImage">
<img
className="Sprite"
onLoad={() => this.setState({ imageLoading: false })}
onError={() => this.setState({ tooManyRequests: true })}
src={this.state.thumbnail}
style={
this.state.tooManyRequests
? { display: "none" }
: this.state.imageLoading
? { display: "null" }
: { display: "null" }
}
/>
</div>
<div className="rightText">
<div className="card-body">
{this.state.title}
<div className="cardButtons">
<button className="btn btn-update btn-outline-primary">Update</button>
<button
onClick={() => adminhelpcard.deleteProduct(this.state.id)}
className="btn btn-outline-primary">
Delete
</button>
</div>
</div>
</div>
</div>
</div>
And then I have the HelpList.tsx module that is responsible for displaying the cards in the form of a list. and the code is as follows:
import React, { Component } from "react";
import HelpCard from "./HelpCard";
import "../help/HelpCard.css";
import axios from "axios";
import InfiniteScroll from "react-infinite-scroller";
import { Button } from "components/ui";
interface State {
url: string;
adminhelpcard: SingleAdminHelpCard[];
error: null;
response: {};
}
interface SingleAdminHelpCard {
id: string;
url: string;
title: string;
thumbnail: string;
}
interface Props {}
export default class HelpList extends Component<Props, State> {
state = {
id: "",
url: "http://localhost:3000/videos/",
adminhelpcard: [],
itemsCountPerPage: 1,
activePage: 1,
error: null,
response: {}
};
loadAdminHelpCard = () => {
axios
.get(this.state.url)
.then((res) => {
this.setState((prevState) => {
const adminhelpcard = prevState.adminhelpcard;
return {
adminhelpcard: [...prevState.adminhelpcard, ...res.data],
url: res.data.next
};
});
})
.catch(function(error) {
// handle error
console.log(error);
});
};
async componentDidMount() {
const apiUrl = "http://localhost:3000/videos/";
const res = await axios.get(this.state.url);
this.setState({ adminhelpcard: res.data });
fetch(apiUrl)
.then((res) => res.json())
.then(
(result) => {
this.setState({
adminhelpcard: result
});
},
(error) => {
this.setState({ error });
}
);
}
deleteProduct(id: any) {
const { adminhelpcard } = this.state;
const apiUrl = `http://localhost:3000/videos/${this.state.id}`;
const options = {
method: "DELETE"
};
fetch(apiUrl, options)
.then((res) => res.json())
.then(
(result) => {
this.setState({
response: result,
adminhelpcard: adminhelpcard.filter((adminhelpcard: SingleAdminHelpCard) => adminhelpcard.id !== id)
});
},
(error) => {
this.setState({ error });
}
);
}
render() {
console.log(this.state.adminhelpcard);
return (
<div>
<React.Fragment>
{this.state.adminhelpcard ? (
<div className="row">
<InfiniteScroll
pageStart={1}
loadMore={this.loadAdminHelpCard}
hasMore={this.state.url ? true : false}
threshold={0}
loader={
<div className="loader" key={0}>
Loading ...
</div>
}>
{this.state.adminhelpcard.map((adminhelpcard: SingleAdminHelpCard, i) => (
<HelpCard
key={adminhelpcard.id + i}
title={adminhelpcard.title}
url={adminhelpcard.url}
thumbnail={adminhelpcard.thumbnail}
/>
))}
</InfiniteScroll>
</div>
) : (
<h1>Loading Cards</h1>
)}
</React.Fragment>
</div>
);
}
}
I get the following error:
This expression is not callable.
Type 'Boolean' has no call signatures.ts(2349)
from the "deleteProduct" function in the line of code:
onClick={() => adminhelpcard.deleteProduct(this.state.id)}
When I try to click the delete button on the helpCards, it says that the no function called "deleteProduct" is recognized. How do I go about fixing this?
--------------------------EDIT----------------------------
Error given for adding deleteProduct to HelpCard component.
"No overload matches this call.
Overload 1 of 2, '(props: Readonly): HelpCard', gave the following error.
Type '{ key: string; title: string; url: string; thumbnail: string; deleteProduct: (id: any) => void; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly & Readonly<{ children?: ReactNode; }>'.
Property 'deleteProduct' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly & Readonly<{ children?: ReactNode; }>'."
First, you need to pass the deleteProduct function as a prop to the HelpCard component.
So in your HelpList.tsx, add another prop to the <HelpCard/> element as follows.
<HelpCard
key={adminhelpcard.id + i}
title={adminhelpcard.title}
url={adminhelpcard.url}
thumbnail={adminhelpcard.thumbnail}
deleteProduct={this.deleteProduct.bind(this)}
/>
Then you need to use it from the onClick handler within the HelpCard component.
<button
onClick={() => this.props.deleteProduct(this.state.id)}
className="btn btn-outline-primary">
Delete
</button>
Probably you may need to change the Props interface in your HelpCard to cater to this new prop as well.
interface Props {
url: string;
title: string;
thumbnail: string;
deleteProduct: (id: any) => void;
}

setState not setting when called from child component

I have a simple app which fetches some weather JSON and displays it. The user can either enter a location or they can hit a "Get lucky" button, which fetches a random city. the initial state is set in App.js
this.state = {
error: '',
status: '',
queryString: 'london,gb',
queryID: '',
queryType: 'q',
cityData: cityData,
weatherData: {},
isLoaded: false
}
Next, I have my main App class, then I have a child component called that contains the form gubbins. I call it in app render as follows:
<SearchForm
queryString={this.state.queryString}
handleChange={this.handleChange}
setQueryType={this.setQueryType}
setQueryID={this.setQueryID}
getWeatherData={this.getWeatherData}
/>
I use callback functions in there to set the query type (location or ID). An example of one of the call back functions in App.js is:
setQueryType = (queryType) => {
this.setState({
queryType: queryType
})
}
This is called in the form JS using:
props.setQueryType(e.target.attributes.query.value)
Now, here is the crux of the issue: the state doesn't update the first time, but DOES on the second click? In fact, other vars like queryString set in the fetch are not set until the second click.
App.js
import React, { Component } from 'react';
import './css/App.css';
import WeatherCard from './components/WeatherCard'
import Header from './components/Header'
import SearchForm from './components/SearchForm'
import cityData from './json/city.list'
const config = {
API: 'https://api.openweathermap.org/data/2.5/forecast',
API_KEY: process.env.REACT_APP_OPEN_WEATHER_MAP_API_KEY
}
class App extends Component {
constructor() {
super()
this.state = {
error: '',
status: '',
queryString: 'london,gb',
queryID: '',
queryType: 'q',
cityData: cityData,
weatherData: {},
isLoaded: false
}
this.getWeatherData()
}
getWeatherData = (searchValue="london,gb") => {
let URL
URL = config.API + '?' + this.state.queryType + '='
URL += this.state.queryType === 'q' ? searchValue : this.state.queryID
URL += '&units=metric&APPID=' + config.API_KEY
console.log(URL)
fetch(URL)
.then( result => result.json() )
.then (
(result) => {
if ( result.cod === '200') {
this.setState({
status: result.cod,
weatherData: result,
queryString: result.city.name,
isLoaded: true
})
} else {
this.setState({
status: result.cod,
error: result.message,
isLoaded: false
})
}
},
(error) => {
this.setState({
isLoaded: false,
error: error
})
}
)
console.log(this.state.queryString)
}
handleChange = (event) => {
const { name, value } = event.target
this.setState({
[name]: value
})
}
getWeatherCards = () => {
let cards = []
for (let i = 0; i < this.state.weatherData.cnt; i++) {
cards.push(
<WeatherCard
key={i}
weatherList={this.state.weatherData.list[i]}
/>
)
}
return cards
}
setQueryType = (queryType) => {
this.setState({
queryType: queryType
})
}
setQueryID = () => {
let randomID = Math.floor(Math.random() * this.state.cityData.length)
let randomCityID = this.state.cityData[randomID].id
this.setState({
queryID: randomCityID
})
}
getlocationForm = () => {
return(
<SearchForm
queryString={this.state.queryString}
handleChange={this.handleChange}
setQueryType={this.setQueryType}
setQueryID={this.setQueryID}
getWeatherData={this.getWeatherData}
/>
)
}
render = () => {
if (this.state.status !== '200') {
return (
<div className='App'>
<Header
status={this.state.status}
error={this.state.error}
/>
{this.getlocationForm()}
</div>
)
} else {
return (
<div className='App'>
{
this.state.isLoaded && (
<Header
cityName={this.state.weatherData.city.name}
countryName={this.state.weatherData.city.country}
status={this.state.status}
error={this.state.error}
/>
)
}
{this.getlocationForm()}
{
this.state.isLoaded && (
<div className='weather-cards'>
{this.getWeatherCards()}
</div>
)
}
</div>
)
}
}
}
export default App;
SearchForm.js
import React from 'react'
const SearchForm = (props) => {
let handleChange = function(e) {
props.handleChange(e)
}
let handleClick = function(e) {
e.preventDefault()
props.setQueryType(e.target.attributes.query.value)
if (e.target.attributes.query.value === 'id') {
props.setQueryID()
}
props.getWeatherData()
}
return (
<div>
<form className="search-form">
<input
type="text"
id="query"
name="query"
placeholder="Enter a location..."
onChange={handleChange}
/>
<button
type="submit"
query="q"
onClick={handleClick}
>
Submit
</button>
<button
type="submit"
query="id"
onClick={handleClick}
>
I'm feeling lucky...
</button>
</form>
</div>
)
}
export default SearchForm
In your App.js constructor add this.setQueryType = this.setQueryType.bind(this)
That line will bind the context of this to the current component, so when called from a child, will update parent state.
I think the problem comes from the fact that when you call getWeatherData,
you don't know if the setState will be over as it is an asynchronous method. (as you can see in the documentation)
So the best way, to ensure that the setState is done before calling your method without being certain of the state of your component, would be to use the callBack parameter of the setState to ensure it runs after the setState method has been finished.
try to put your this.getWeatherData() into the componentDidMount and remove it from the constructor
componentDidMount() {
this.getWeatherData()
}

ReactJS | Loading State in component doesn't render Spinner

I am trying to make a React component that displays multiple renders based on props and state. So, while I wait for the promise to be resolved, I want to display a spinner Component
Main Renders:
NoResource Component => When the user is not valid
Spinner Component => When is loading on all renders
BasicRender Component => When data are fetched and is not loading
Below is my component:
/* eslint-disable react/prefer-stateless-function */
import React, { Component, Fragment } from 'react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { getUser, listUsers } from '../../config/service';
export class UserDetailsScreen extends Component {
static propTypes = {
match: PropTypes.shape({
isExact: PropTypes.bool,
params: PropTypes.object,
path: PropTypes.string,
url: PropTypes.string
}),
// eslint-disable-next-line react/forbid-prop-types
history: PropTypes.object,
label: PropTypes.string,
actualValue: PropTypes.string,
callBack: PropTypes.func
};
state = {
user: {},
error: '',
isloading: false
};
componentDidMount() {
this.fetchUser();
this.setState({ isLoading: true})
}
getUserUsername = () => {
const { match } = this.props;
const { params } = match;
return params.username;
};
fetchUser = () => {
getUser(this.getUserUsername())
.then(username => {
this.setState({
user: username.data,
isloading: false
});
})
.catch(({ message = 'Could not retrieve data from server.' }) => {
this.setState({
user: null,
error: message,
isLoading: false
});
});
};
validateUsername = () =>
listUsers().then(({ data }) => {
const { match } = this.props;
if (data.includes(match.params.username)) {
return true;
}
return false;
});
// eslint-disable-next-line no-restricted-globals
redirectToUsers = async () => {
const { history } = this.props;
await history.push('/management/users');
};
renderUserDetails() {
const { user, error } = this.state;
const { callBack, actualValue, label, match } = this.props;
return (
<div className="lenses-container-fluid container-fluid">
<div className="row">
.. More Content ..
{user && <HeaderMenuButton data-test="header-menu-button" />}
</div>
{user && this.validateUsername() ? (
<Fragment>
.. Content ..
</Fragment>
) : (
<div className="container-fluid">
{this.renderNoResourceComponent()}
</div>
)}
<ToolTip id="loggedIn" place="right">
{user.loggedIn ? <span>Online</span> : <span>Oflline</span>}
</ToolTip>
</div>
);
}
renderNoResourceComponent = () => {
const { match } = this.props;
return (
<div className="center-block">
<NoResource
icon="exclamation-triangle"
title="Ooops.."
primaryBtn="« Back to Users"
primaryCallback={this.redirectToUsers}
>
<h5>404: USER NOT FOUND</h5>
<p>
Sorry, but the User with username:
<strong>{match.params.username}</strong> does not exists
</p>
</NoResource>
</div>
);
};
renderSpinner = () => {
const { isLoading, error } = this.state;
if (isLoading && error === null) {
return <ContentSpinner />;
}
return null;
};
render() {
return (
<div className="container-fluid mt-2">
{this.renderSpinner()}
{this.renderUserDetails()}
</div>
);
}
}
export default withRouter(UserDetailsScreen);
The problem is:
I get the spinner along with the main component, and I am getting this error:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.. Can you please tell me what I am doing wrong.
The error is because you are running the renderUserDetailsComponent even when your API call is in loading state. You must only render the spinner on loading state
renderUserDetails() {
const { user, error, isLoading } = this.state;
if(isLoading) {
return null;
}
const { callBack, actualValue, label, match } = this.props;
return (
<div className="lenses-container-fluid container-fluid">
<div className="row">
.. More Content ..
{user && <HeaderMenuButton data-test="header-menu-button" />}
</div>
{user && this.validateUsername() ? (
<Fragment>
.. Content ..
</Fragment>
) : (
<div className="container-fluid">
{this.renderNoResourceComponent()}
</div>
)}
<ToolTip id="loggedIn" place="right">
{user.loggedIn ? <span>Online</span> : <span>Oflline</span>}
</ToolTip>
</div>
);
}

React Compound Components type warning

I have a simple compound component with a bunch of static subcomponents:
// #flow
import React, { Component, Children } from 'react';
type Props = {
children: React.ChildrenArray<React.Node> | React.Node,
}
class Toggle extends Component<Props> {
static On = props => (props.on ? props.children : null);
static Off = props => (props.on ? null : props.children);
static Button = props => (
<button
onClick={props.toggle}
type="button"
style={{ display: 'inline-block' }}
>
<pre>{JSON.stringify(props.on, null, 2)}</pre>
</button>
);
state = { on: false }
toggle = () => {
this.setState(
({ on }) => ({ on: !on }),
// maybe this.props.someCallback
() => console.log(this.state.on),
);
}
render() {
return Children.map(
this.props.children,
childElem => React.cloneElement(childElem, {
on: this.state.on,
toggle: this.toggle,
}),
);
}
}
export default Toggle;
The warning happens when I try to put some other elements into Toggle children scope.
For example:
<Toggle>
<Toggle.On>On</Toggle.On>
<span /> <-- this is savage
<Toggle.Button />
<Toggle.Off>Off</Toggle.Off>
</Toggle>
Everything is working, but my flowtype warn me about this span like so:
Warning: Received `false` for a non-boolean attribute `on`.....
Warning: Invalid value for prop `toggle` on <span> tag....
How can I to pacify this nasty girl?
Thank you guys, I think, right solution is just check if type of mounted node is correct one, otherwise - just clone node with regular node props:
// #flow
import React, { Component, Children } from 'react';
type Props = {
children: React.ChildrenArray<React.Node> | React.Node,
}
class Toggle extends Component<Props> {
static On = props => (props.on ? props.children : null);
static Off = props => (props.on ? null : props.children);
static Button = props => (
<button
onClick={props.toggle}
type="button"
style={{ display: 'inline-block' }}
>
<pre>{JSON.stringify(props.on, null, 2)}</pre>
</button>
);
state = { on: false }
toggle = () => {
this.setState(
({ on }) => ({ on: !on }),
// maybe this.props.someCallback
() => console.log(this.state.on),
);
}
// Checking here
allowedTypes = ({ type }) => {
return [
(<Toggle.On />).type,
(<Toggle.Off />).type,
(<Toggle.Button />).type,
].includes(type);
}
render() {
return Children.map(
this.props.children,
(childElem) => {
const elemProps = this.allowedTypes(childElem) ? {
on: this.state.on,
toggle: this.toggle,
} : childElem.props;
return React.cloneElement(childElem, elemProps);
},
);
}
}
export default Toggle;
You can also do this, just having the components in a list and checking their type inside .map, putting on the custom props or otherwise just returning the original child.
const allowedTypes = [ToggleOn, ToggleOff, ToggleButton]
return React.Children.map(props.children, child => {
if (allowedTypes.includes(child.type)) {
return React.cloneElement(child, {on, toggle})
}
return child
})
}

Categories