In my stores, I have functions set to handle errors in API calls.
One example is for authorization - if a user cannot view a project, they get an unauthorized error.
In my store, I set an error property on the state.
Then, in my component's render function, I first check for this.state.error. This works fine - I render a reused error component containing the error message and code.
The problem is that I need to reset the error state to null after the user moves on - yet resetting the state causes the component to re-render.
My current approach (in my component's render function):
if (this.state.error) {
return (
<Error
errorTitle={this.state.errorCode}
errorMessage={this.state.error}
clearErrors={this.clearErrors}
/>
)
}
And a function that belongs to the same class:
clearErrors: function () {
this.setState({
error: null,
errorCode: null
});
},
And then my Error component:
var Error = React.createClass({
clearErrors: function () {
this.props.clearErrors();
},
render: function () {
return (
<Panel className='errorPanel' header={this.props.errorTitle} bsStyle='danger'>
<p>{this.props.errorMessage}</p>
<a href='#/dashboard'>
<Button onClick={this.clearErrors}>Return to Dashboard</Button>
</a>
</Panel>
)
}
});
The problem is evident - before the onClick actually returns my user to the dashboard, it very quickly renders the component that the user is not supposed to be able to see.
How should I be handling this?
The errorcode should not in the components state. Because (apparently) it does not correspond fully with the components state. From what I gather you are looking for is the following states:
Error code = not null: error component displayed
Error code reset to null: error component still displayed (because you do NOT want to re-render the component)
So I would suggest removing error from state and do something like:
render() {
errorcode = this.props.errorcode;
if (this.props.errorcode) {
return <ErrorComponent...>
}
clearErrors() {
errorcode = null;
}
Related
I want to be able to receive mqtt messages and display them on a web app. I'm working with AWS Amplify's PubSub and the function calls happen outside of the class component. I can't directly access any of the instance functions/properties from the function outside of it but I need some way to trigger a setState change so that the web app can be re-rendered, right?
I've tried just directly calling the function from the React class, but there's still no re-rendering happening. I've tried making an outside variable, storing the message received in it and then accessing the variable from the React class but still no trigger. Then I researched and found that I could force a re-render every couple of seconds but read that doing such a thing should be avoided. I should be using ComponentDidMount and setState functions but not really understanding how to get this to work.
Again all I'm really trying to do is get the message and update the web app to display the new message. Sounds pretty simple but I'm stuck.
import...
var itemsArr = [];
function subscribe() {
// SETUP STUFF
Amplify.configure({
...
});
Amplify.addPluggable(new AWSIoTProvider({
...
}));
// ACTUAL SUBSCRIBE FUNCTION
Amplify.PubSub.subscribe('item/list').subscribe({
next: data => {
// LOG DATA
console.log('Message received', data);
// GET NAMES OF ITEMS
var lineItems = data.value.payload.lineItems;
lineItems.forEach(item => itemsArr.push(item.name));
console.log('Items Ordered', itemsArr);
// THIS FUNCTION CALL TRIGGERS AN ERROR
// CANT ACCESS THIS INSTANCE FUNCTION
this.update(itemsArr);
},
error: error => console.error(error),
close: () => console.log('Done'),
});
}
// REACT COMPONENT
class App extends Component {
constructor(props){
super(props);
this.state = {
items:null,
};
}
// THOUGHT I COULD USE THIS TO UPDATE STATE
// TO TRIGGER A RE-RENDER
update(stuff){
this.setState({items: stuff});
}
render() {
// THINK SUBSCRIBE METHOD CALL SHOULDN'T BE HERE
// ON RE-RENDER, KEEPS SUBSCRIBING AND GETTING
// SAME MESSAGE REPEATEDLY
subscribe();
console.log('items down here', itemsArr);
return (
<div className="App">
<h1>Test</h1>
<p>Check the console..</p>
<p>{itemsArr}</p>
</div>
);
}
}
export default App;
Ideally, I'd like columns of the list of items to be displayed as messages come in but I'm currently getting an error - "TypeError: Cannot read property 'update' of undefined" because the subscribe function outside of the class doesn't have access to the update function inside the class.
Put subscribe method inside the App component so you can call it. You can call subscribe method in componentDidMount lifecycle to execute it (to get the items) after App component renders the first time. And then, update method will run this.setState() (to store your items in the state) causing App component to re-render. Because of this re-render, your this.state.items will contain something and it will be displayed in your paragraph.
class App extends Component {
constructor(props){
super(props);
this.state = {
items: [],
};
this.subscribe = this.subscribe.bind(this);
this.update = this.update.bind(this);
}
update(stuff){
this.setState({ items: stuff });
}
subscribe() {
// SETUP STUFF
Amplify.configure({
...
});
Amplify.addPluggable(new AWSIoTProvider({
...
}));
// ACTUAL SUBSCRIBE FUNCTION
Amplify.PubSub.subscribe('item/list').subscribe({
next: data => {
// LOG DATA
console.log('Message received', data);
// GET NAMES OF ITEMS
let itemsArr = [];
var lineItems = data.value.payload.lineItems;
lineItems.forEach(item => itemsArr.push(item.name));
console.log('Items Ordered' + [...itemsArr]);
this.update(itemsArr);
},
error: error => console.error(error),
close: () => console.log('Done'),
});
}
componentDidMount() {
this.subscribe();
}
render() {
console.log('items down here ' + [...this.state.items]);
return (
<div className="App">
<h1>Test</h1>
<p>Check the console..</p>
<p>{this.state.items !== undefined ? [...this.state.items] : "Still empty"}</p>
</div>
);
}
}
export default App;
By using an Object as a prop which stores references to internal methods of a child-component, it is possible to then access them from the parent.
The below (overly-simplified) example shows this.
The RandomNumber purpose is to generate a single random number. It's all it does.
Then, at its parent-level, some user action (button onClick event) is triggering the RandomNumber component to re-render, using a custom hook called useShouldRender, which generates a random number every time it's setter function is invoked, so by exposing the setter function to the "exposed" prop object, it is possible to interact with internal component operations (such as re-render)
const {useState, useMemo, useReducer} = React
// Prints a random number
const RandomNumber = ({exposed}) => {
// for re-render (https://stackoverflow.com/a/66436476/104380)
exposed.reRender = useReducer(x => x+1, 0)[1];
return Math.random(); // Over-simplification. Assume complex logic here.
}
// Parent component
const App = () => {
// create a memoed object, which will host all desired exposed methods from
// a child-component to the parent-component:
const RandomNumberMethods = useMemo(() => ({}), [])
return (
<button onClick={() => RandomNumberMethods.reRender()}>
<RandomNumber exposed={RandomNumberMethods}/>
</button>
)
}
// Render
ReactDOM.render(<App />, root)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Before I load my React App I need to check 2 conditions
User is Logged in, if not redirect to login page
All of the
User Settings fetched using API, if not display a loading screen.
So, inside render method, I have below conditions:
if (!this.isUserLoggedIn()) return <NotifyPleaseLogin />;
else if (!this.state.PageCheck) {
return (
<PageLoading
clientId={Config.clientId}
setPageReady={this.setPageReady()}
/>
);
} else {
return "Display the page";
In this scenario, what I expect to see happen is that, if user is not logged in, user redirected to login page. If user logged in and currently page is fetching the API query, user will see the PageLoading component (loading screen) and lastly if page is ready, the page will get displayed.
Right now I am getting Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state. error, which is because I am doing a setState update within Render method of the parent and also I am getting TypeError: props.setPageReady is not a function at PageLoading.js:29 error when I try to run parent's function that sets the state of PageReady to true like below
setPageReady() {
this.setState({ PageCheck: true });
}
How can I set this up so child can display a loading page until the page is ready (During this child can do an API call and retrieve user settings) then let parent know all settings are retrieved and are in the redux so parent can proceed loading the page?
You can easily achieve this by adding more states to actively control your component:
state = {
isAuthorized: false,
pagecheck: false
};
We move the authorization check to a lifecylcle-method so it doesn't get called every render.
componentDidMount() {
if(this.isUserLoggedIn()) {
this.setState({
isAuthorized: true
});
}
}
Using our state, we decide what to render.
render() {
const {
pagecheck,
isAuthorized
} = this.state;
if(!isAuthorized){
return <NotifyPleaseLogin />;
}
if(!pagecheck) {
return (
<PageLoading
clientId={Config.clientId}
setPageReady={() => this.setPageReady()}
/>
);
}
return "Display the page";
}
Note: Previously you passed this.setPageReady() to Pageloading. This however executes the function and passes the result to Pageloading. If you want to pass the function you either need to remove the braces this.setPageReady or wrap it into another function () => this.setPageReady()
You can pass PageCheck as prop from Parent to and show/hide loader in component based on that prop.
<PageLoading
clientId={Config.clientId}
pageCheck={this.state.PageCheck}
setPageReady={this.setPageReady}
/>
Then call setPageReady inside the success and error of the API call that you make in the child function:
axios.get(api)
.then((response) => {
//assign or do required stuff for success
this.props.setPageReady();
})
.catch((error) => {
//do error related stuff
this.props.setPageReady(); //as you need to hide loader for error condition as well
})
state = {
isAuthorized: false,
pageCheck: false
};
componentDidMount() {
if(this.isUserLoggedIn()) {
this.setState({
isAuthorized: true
});
}
}
{!this.state.isAuthorized ?
<NotifyPleaseLogin />
:
(!this.state.pageCheck ?
<PageLoading
clientId={Config.clientId}
setPageReady={this.setPageReady()}
/>
:
"Display the page")
}
When fetching data I'm getting:
Can't perform a React state update on an unmounted component.
The app still works, but react is suggesting I might be causing a memory leak.
I've searched a lot for a solution to this warning and made many changes (including using an isMounted state and an abortController) but haven't found anything which fixes this for me as of yet. This is due to the fact that I want to the output of the API to be a variable or constant so that I can use it in another class.
componentDidMount() {
fetch("https://jirareportsforsolutions.azurewebsites.net/boards")
.then(response => response.json())
.then(
(result) => this.setState(boards = JSON.parse(JSON.stringify(result, null, 2).slice(82, -1)))
)
// console.log(boards)
}
componentDidUpdate() {
if (this.state.selectValues.length > 0)
fetch("https://jirareportsforsolutions.azurewebsites.net/sprints/" + boardId)
.then(response => response.json())
.then(
(result) => this.setState(sprints = JSON.parse(JSON.stringify(result, null, 2).slice(68, -1)))
)
// console.log(sprints)
}
}
This is the render component where 'boards' is being used within a dropdown (using the react-dropdown-select package) and 'sprints' is being passed to a different class(StartSprintDropdown):
render() {
if (boards == null) {
noData = "No boards available"
}
return (
<div>
<div className="boardDropdown">
<StyledSelect
placeholder="Select a board"
color={this.state.color}
searchBy={this.state.searchBy}
clearable={this.state.clearable}
searchable={this.state.searchable}
dropdownHandle={this.state.handle}
dropdownHeight={this.state.dropdownHeight}
direction={this.state.direction}
labelField={this.state.labelField}
valueField={this.state.valueField}
options={boards}
dropdownGap={this.state.gap}
keepSelectedInList={this.state.keepSelectedInList}
onChange={values => this.setValues(values)}
noDataLabel={noData}
closeOnSelect={this.state.closeOnSelect}
dropdownPosition={this.state.dropdownPosition}
/>
</div>
<div>
<StartSprintDropdown sprints={sprints} boardId={boardId} projectId={projectId} sprintError={sprintError}/>
</div>
</div>
);}
}
This is the full warning that is shown:
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.
I'm not sure if I have assigned the 'boards' and 'sprints variables correctly as they are not states but the app works fine, although the second fetch (within the componentDidUpdate) constantly fetches once triggered.
Since I'm new to React any help with this and my code is greatly appreciated.
This could be happening because somewhere along the line, your component is getting destroyed, then the async call to this.setState gets run and prints this error message.
Try adding this to your class to see if your component is being destroyed too early:
componentWillUnmount() {
console.log('unmounting!!!!')
}
I receive the "Uncaught TypeError: Cannot read property 'data' of null" error in the console when I'm loading up my page.
In the constructor I call an external api for data with axios using redux-promise, then I pass props down to a stateless component (TranslationDetailBox).
The data arrives, and the child component will receive the props shortly (page looks ok), but first the error message appears.
this.props.translation is empty before the api call, and rendering happens before the state is updated with the external data. I believe this is the issue, but I have no clue on how to solve it.
class TranslationDetail extends Component {
constructor(props){
super(props);
this.props.fetchTrans(this.props.params.id);
}
render() {
return (
<div className="container">
<TranslationDetailBox text={this.props.translation.data.tech}/>
<TranslationDetailBox text={this.props.translation.data.en}/>
</div>
);
}
}
function mapState(state) {
const { translation } = state;
return { translation };
}
...
I find this to be a good use case for conditional rendering.
Your render could check to see whether the data has loaded or not and if not, render something indicating it is still loading.
A simple implementation could be:
render() {
return (
!this.props.data ?
<div>Loading...</div>
:
<div>{this.props.data}</div>
)
}
You could set defaultProps:
TranslationDetail.defaultProps = {
translation: {
data: {}
}
};
I am trying to build an app that uses drag-and-drop behaviour, and the component being dragged needs to be cloned elsewhere in the DOM. Since the component is already mounted, trying to mount it again causes the browser to hang.
Trying to use cloneWithProps results in a Cannot read property 'defaultProps' of undefined error.
Here's a testcase:
var TestCase = React.createClass({
getInitialState () {
return {
draggingItem: null
}
},
render () {
return <div>
<ExampleComponent onClick={this.setDraggingItem} />
{this.state.draggingItem}
</div>
},
setDraggingItem (component) {
// This gives `Cannot read property 'defaultProps' of undefined`
//React.addons.cloneWithProps(component)
// This crashes the browser
//this.setState({ draggingItem: component })
}
})
var ExampleComponent = React.createClass({
render () {
return <div onClick={this.handleOnClick}>Hello World</div>
},
handleOnClick (event) {
this.props.onClick(this)
}
})
React.render(<TestCase />, document.body)
Of course I could simply clone component.getDOMNode() in setDraggingItem, but it really seems like rendering the component or calling cloneWithProps should work?
The two things you need to create an element is: the component class (e.g. ExampleComponent) and its props. cloneWithProps is only to be used in render and only with an element coming from props which was created in another component's render. You shouldn't save elements, or pass them around other than to other components in render. Instead, you pass around objects (props) and component classes.
Since you need to know the props and component class to render it in the first place, you can handle all of this in TestCase.
var TestCase = React.createClass({
getInitialState () {
return {
draggingItem: null,
draggingItemProps: null
}
},
render () {
return <div>
<ExampleComponent onClick={this.setDraggingItem.bind(null,
/* the component class */ ExampleComponent,
/* the props to render it with */ null
)} />
{
this.state.draggingItem && React.createElement(
this.state.draggingItem,
this.state.draggingItemProps
)
}
</div>
},
setDraggingItem (component, props, event) {
this.setState({ draggingItem: component, draggingItemProps: props })
}
});
var ExampleComponent = React.createClass({
render () {
return <div onClick={this.handleOnClick}>Hello World</div>
},
// just defer the event
handleOnClick (event) {
this.props.onClick(event)
}
});
If you wish to make these valid outside this TestCase component, ensure there aren't any functions bound to TestCase in the props. Also ensure there's no children prop with react elements in it. If children are relevant, provide the {componentClass,props} structure needed to recreate them.
It's hard to tell what your actual requirements are, but hopefully this is enough to get you started.
You need be sure you're creating a new component with the same props, not mount the same one multiple times. First, setup a function that returns an instantiated components (easier to drop JSX here):
function getComponent(props) {
return ExampleComponent(props);
}
Then in your TestCase render:
return (<div>
{ getComponent({ onClick: this.setDraggingItem }) }
{ this.state.draggingItem }
</div>);
That will create the first component. Then to create a clone:
setDraggingItem(component) {
var clone = getComponent(component.props);
}
This deals with the cloning part. You still have some dragging and rendering to figure out.