I am just fetching the items from server and displaying them in the list component. The component structure is like
ServiceScreen(parent) -> ServicesList(child- used inside ServiceScreen) -> ServiceItem (child used inside ServicesList)
Every thing works well and it displays the services fetched but it gives me the following warning
Warning: Can only update a mounted or mounting component. This usually
means you called setState, replaceState, or forceUpdate on an
unmounted component. This is a no-op. Please check the code for the
YellowBox component.
Code is as follows
ServiceScreen.js
constructor(props) {
super(props);
this.props.actions.fetchServices();
}
render() {
const { isFetching } = this.props;
return (
<View style={styles.container}>
<ServicesList services={this.props.services} navigation={this.props.navigation} />
</View>
);
}
ServicesList.js
render() {
return (
<View style={{ flex: 1 }}>
<FlatList
data={this.props.services}
renderItem={
({ item }) =>
<ServiceItem navigation={this.props.navigation} item={item} />
}
/>
</View>
);
}
ServiceItem.js
render() {
const { item } = this.props;
return (
<TouchableOpacity onPress={this.singleService}>
<Text>{item.service_name}</Text>
</TouchableOpacity>
);
}
As I am using redux for state management, I have mapped the services
state to my ServiceScreen component. And I am passing down it to child
component.
You should dispatch the action in componentDidMount. The constructor is called before the component is mounted, which is why you're seeing the error.
Here's a diagram with the react lifecycle. You should only call methods that change state in the "commit" phase.
This error is firing exactly when said in the yellowBox. Somewhere you trying to update already unmounted component.
I handled this issue by using temporary variable componentIsMounted: bool and wrapping setState method:
class SomeClass extends Component {
constructor(props) {
this._isComponentMounted = false;
}
componentDidMount() {
this._isComponentMounted = true;
}
componentWillUnmount() {
this._isComponentMounted = false;
}
setState(...args) {
if (!this._isComponentMounted) {
return;
}
super.setState(...args);
}
}
Also, you should remember that you should not call state updates before the component get mounted (it`s not an alternative way - just an addition).
Related
I'm trying to move from componentWillReceiveProps to getDerivedStateFromProps and in some cases, I was successful but when the case is append the props to the existing state, then things start to not behaving the same way. When a make an update to the component's state, the state changes (and the component did after updated) but still renders the previous state. Something weird happens when using getDerivedStateFromProp instead of componentWillReceiveProps. It seems that method does not handle well with 'internal' changes. In the following example, I have getDerivedStateFromProp on Child and it works, but because is only rendering the props. This behavior was also observed in a simpler example where I didn't have any child components and was just rendering state changes.
The code below shows a child component that is used to print/show the data received by props while uses a delete data handler (to remove data that is stored at Parent from child component interaction). When using getDerivedStateFromProps() I can't access to this.state and the prevState doesn't mean the same since the state is accumulative. And when I remove data from the child component, doesn't update the props of the child (while using componentWillReceiveProps was OK). So, I do not find a way to substitute my UNSAFE_componentWillReceiveProps
componentWillReceiveProps:
UNSAFE_componentWillReceiveProps(nextProps){
this.setState({
data: [...this.state.data,...nextProps.data]
})
}
getDerivedStateFromProps:
static getDerivedStateFromProps(nextProps,state) {
if (!isEqual(nextProps.data, state.data)) {
return {
data: [...state.data, ...nextProps.data]
};
}
return null;
}
The original code that works as intended (before getDerivedStateFromProps on Parent Comp.)
DataConsole - Parent Component:
export class DataConsole extends Component {
// Used for unsubscribing when our components unmount
unsub = null;
static defaultProps = {
data: [],
};
constructor(props) {
super(props);
this.state = {
data: [],
};
this.handleTableRowClick = this.handleTableRowClick.bind(this);
}
UNSAFE_componentWillReceiveProps(nextProps){
this.setState({
data: [...this.state.data,...nextProps.data]
})
}
handleTableRowClick(key) {
console.log(
"handleTable",
key,
this.state.data[key],
this.state.data.length
);
const e = this.state.data.splice(key, 1)
//console.log("remove?", e , this.state.data.length)
this.setState({
undoDataRemove: e
});
}
render() {
return (
<div>
<Container
fluid
style={{ paddingLeft: 0, paddingRight: 0 }}
className="DataContainer"
>
<Row noGutters>
<Col sm={8} className="ConsoleTable">
<div>
<DataViewer
data={this.state.data}
rowClickHandler={this.handleTableRowClick}
/>
</div>
...
DataViewer - Child Component
import isEqual from "react-fast-compare";
...
export class DataViewer extends Component {
static defaultProps = {
data: [],
};
constructor(props){
super(props)
this.state={data: []}
}
componentDidUpdate() {
console.log("DataViewer updated");
}
static getDerivedStateFromProps(nextProps, prevProps) {
console.log(nextProps, prevProps)
if (!isEqual(nextProps.data, prevProps.data)) {
return {
data: nextProps.data
};
}
return null;
}
render() {
return (
<div className={"TableData"}>
<Table responsive="lg" striped borderless hover>
<tbody>
{this.state.data.map((elem, ids) => {
if (!isEmpty(elem)) {
return (
<tr key={ids} onClick={() => this.props.rowClickHandler(ids)}>
<td>{ids + 1}</td>
{Object.keys(elem).map(function (value, idx) {
return (
<td key={idx}>
{value}:{elem[value]}
</td>
);
})}
</tr>
);
} else {
return null;
}
})}
</tbody>
</Table>
</div>
);
}
}
There is a bug in your code that causes your problem, and it is unrelated to getDerivedStateFromProps and UNSAFE_componentWillReceiveProps.
The faulty line is this:
const e = this.state.data.splice(key, 1)
It changes this.state.data without calling setState. Never do that. The only way you are ever allowed to make any changes to this.state is via this.setState or by returning something to be merged into state from getDerivedStateFromProps, not any other way.
In order to more easily cope with changes deep in your state tree, immutable libraries come in handily. immer is currently among the top candidates in this category. Using immer, you can modify state any way you want, as long as you wrap it into a produce call using the pattern this.setState(produce(this.state, newState => { /* change newState here */ })):
import produce from 'immer';
// ...
this.setState(produce(this.state, newState => {
const e = newState.data.splice(key, 1);
newState.undoDataRemove = e;
}));
Hope, this can help.
I'm tracking when componentDidUpdate and render are firing with log statements.
The log statements in componentDidUpdate do not fire after render. I have used breakpoints to confirm this isn't a timing issue.
I'm using "render props" to wrap the component in question. My code (stripped down) is below. This is the output of the logging. Sometimes I'll get componentDidUpdate to fire, but inconsistently and it's never the final thing, a RENDER always shows up in my logs last, never UPDATE.
As I understand it componentDidUpdate should fire even if the update does not modify the DOM (though the renders here do update the DOM.) I've tried React#16.11.x and React#16.12.x with identical results.
class MyWrapper extends React.PureComponent {
render() {
const { buttonDefinitions } = this.props;
return (
<InfoProvider
render={infoProps => {
return (
<MyMenu
{...{ buttonDefinitions, infoProps }}
/>
);
}}
/>
);
}
}
class MyMenu extends React.Component {
componentDidUpdate() {
log.warn('UPDATE');
}
render() {
log.warn('RENDER');
const { buttonDefinitions } = this.props;
return (
<MenuWrapper>
{buttonDefinitions.map(buttonDef => (
<MyButton {...buttonDef} />
))}
</MenuWrapper>
);
}
}
As per react docs, if you are using render props with React pure component, then shallow prop comparison will always return false for new props. In this case, for each render it will generate a new value for the render prop. As new props getting created & not updating previous one it won't call componentDidUpdate.
I have been using react 16.2 and the context api works as expected with the following structure.
const MyContext = createContext(null);
class Provider extends Component {
state = {
id: 'A value I set'
}
onUpdate = (e) => {
const { id } = e.dataset
this.setState({ id });
}
render() {
const { children } = this.props;
const { id } = this.state;
return(
<MyContext.Provider value={id}>
{children}
</MyContext.Provider>
)
}
}
function ConsumerHOC(WrappedComponent) {
function renderWrappedComponent(id) {
return <WrappedComponent id={id} />
}
return (
<MyContext.Consumer>
{renderWrappedComponent}
</MyContext.Consumer>
)
}
When I switched to react 16.8, this code breaks down. Whenever onUpdate is invoked, the provider value is updated. However, the consumer never receives the updated value.
If I record a timeline, I can see that react's internal propagateContextChange method is called, but nothing happens after that.
I answered it on my own! I had only upgraded react, and forgot to also upgrade react-dom. The new version of react relies on the new version of react-dom, so the upgrade broke react context.
In 16.8, you'll need to use the context's consumer via a render prop like this...
<MyContext.Consumer>
{ context => <YourComponent {...context} /> }
</MyContext.Consumer>
This will require you to refactor your HOC as thus...
function ConsumerHOC(WrappedComponent) {
function renderWrappedComponent(id) {
return (
<MyContext.Consumer>
{ value => <WrappedComponent id={id} contextValue={value} /> } // if you don't want to pass through all the context's properties, you can chose which props to pass to the wrapped component
</MyContext.Consumer>
)
}
return renderWrappedComponent
}
In my project, I'm looping through cards in my Child Component, and updating the Parent's state with ones where I activate an event of 'swiping right' on the card to favorite. The parent's state keeps track of all favorites by adding the favorited card to the array.
I'm passing down a function (updatestate)from the Parent Component, App, to the Child, Main, that allows the Child to call .setState() and append to the array in the Parent state.
But, when I activate the eventhandler onSwipedRight inside the Child Component, the parent's state gets updated as planned with the new Card, but nothing below the <Card> in Main gets rendered automatically for the next card, as it should. If I tap the screen, then the next card/picture renders only then.
Any ideas? Am I missing some binding or need to do some componentDidMount or anything so the code in child component, Main, renders even after I activate the event handler that updates the parent state?
Basically, is there a tool in React to make sure something renders or at least waits for it to render? (post event handling which sets the parents state in my case)
collection = imagedata;
//collection is the data (local JSON) i'm looping thru via .map in Main Component
const RootStack = StackNavigator(
{
Main: {
screen: Main}
}
);
export default class App extends Component<{}> {
constructor(props) {
super(props);
this.state = {
favoritesList: []
};
}
updateArr=(itemname, url)=>{this.setState({ favoritesList: [...this.state.favoritesList, {"item_name":itemname, "url":url}]})};
render() {
return <RootStack screenProps={{appstate: this.state,
updatestate: this.updateArr}}
/>;
}
}
class Main extends React.Component {
render() {
var updatestate = this.props.screenProps.updatestate;
const contents = collection.map((item, index) => {
return (
<Card key={index}
onSwipedRight={() => {updatestate(item.item_name,item.url)}}
>
<View> //THIS and anything after <Card> doesn't render for the next card automatically if I do 'onSwipedRight'
<Image
source={{uri: item.url}} />
</View>
</Card>
)
},this);
return (
<View>
<CardStack>
{contents}
</CardStack>
</View>
);
}
}
(abbreviated) Project structure:
App
|__ Rootstack
|
|__Main
UPDATE (more info):
Just to test the event handler, I added in a function that doesn't set the state of the parent, and had <Card> call that on the event handler -- it works perfectly and the child component <Card> renders perfectly. It seems that it's the updatestate function passed down from the parent to the child that acts to call .setState() upstream that for some reason is causing the Child to not render/not finish rendering after the event handler.
class Main extends React.Component {
render() {
var updatestate = this.props.screenProps.updatestate;
var newfunc = (a, b) => {console.log('a:', a, 'b:', b)};
const contents = collection.map((item, index) => {
return (
<Card key={index}
newfunc(item.item_name,item.item_name);}}
// onSwipedRight={() => {updatestate(item.item_name,item.url); }}
>
If you need to set the state of a component based on the previous state, you should use
this.setState((prevState, props) => {})
Which should, in your case look like
this.setState((prevState, props) => {
return {
favoritesList: [
...prevState.favoritesList,
{"item_name":itemname, "url":url}
]
};
})
For more on setState
I'm creating a custom Navigator component. I need to provide Navigator's stack components a navigator prop to allow them to push and pop scenes like this:
this.props.navigator.push(<Product id='4815'>)
this.props.navigator.pop()
In order to achieve this result, inside my Navigator's class, I've used React.cloneElement():
class Navigator extends Component {
constructor(props) {
super(props)
this.state = { stack: [this._addNavigator(props.children)] }
}
_addNavigator(scene) {
return React.cloneElement(scene, {
navigator: {
push: this.push,
pop: this.pop,
popToRoot: this.popToRoot
}
})
}
push(scene) {
this.setState({
stack: [...this.state.stack, this._addNavigator(scene)]
})
}
...
}
Everything works just fine, except for a particular scenario.
class App extends Component {
constructor(props) {
super(props)
this.state = { toggle: false }
}
render() {
return (
<View>
<TouchableOpacity onPress={() => {
this.setState({ toggle: !this.state.toggle })
}}>
<Text>Toggle</Text>
</TouchableOpacity>
<Navigator>
<SampleScene backgroundColor={this.state.toggle ? 'green' : 'black'} />
</Navigator>
</View>
)
}
When I pass some mutable prop to the Navigator children, as soon as the prop changes, the child component does not rerender. In the example above, SampleScene's backgroundColor stays black (because App class initial state for toggle is set to false), despite the user pressing the Toggle button. It seems like the SampleScene's render() method is called just once. How could I troubleshoot this behaviour?
Problem solved. Inside Navigator, I had to intercept new props via componentWillReceiveProps. Setting the stack to newProps' children via setState method made the Navigator rerender properly.