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.
Related
Background
I am trying to make an element disappear after the animation ends (I am using animate.css to create the animations).
The above 'copied' text uses animated fadeOut upon clicking the 'Copy to Journal Link'. Additionally, the above demo shows that it takes two clicks on the link to toggle the span containing the text 'copied' from displayed to not displayed.
According to the animate.css docs, one can also detect when an animation ends using:
const element = document.querySelector('.my-element')
element.classList.add('animated', 'bounceOutLeft')
element.addEventListener('animationend', function() { doSomething() })
My Problem
However, within the componentDidMount() tooltip is null when attempting to integrate what animate.css docs suggest.
What am I doing wrong? Is there a better way to handle this behavior?
ClipboardBtn.js
import React, { Component } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
class ClipboardBtn extends Component {
constructor(props) {
super(props)
this.state = {
copied: false,
isShown: true,
}
}
componentDidMount() {
const tooltip = document.querySelector('#clipboard-tooltip')
tooltip.addEventListener('animationend', this.handleAnimationEnd)
}
handleAnimationEnd() {
this.setState({
isShown: false,
})
}
render() {
const { isShown, copied } = this.state
const { title, value } = this.props
return (
<span>
<CopyToClipboard onCopy={() => this.setState({ copied: !copied })} text={value}>
<span className="clipboard-btn">{title}</span>
</CopyToClipboard>
{this.state.copied ? (
<span
id="clipboard-tooltip"
className="animated fadeOut"
style={{
display: isShown ? 'inline' : 'none',
marginLeft: 15,
color: '#e0dbda',
}}
>
Copied!
</span>
) : null}
</span>
)
}
}
export default ClipboardBtn
Using query selectors in React is a big NO. You should NEVER do it. (not that that's the problem in this case)
But even though it's not the problem, it will fix your problem:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
https://reactjs.org/docs/refs-and-the-dom.html
componentDidMount gets called only once during the inital mount. I can see that in the inital component state, copied is false, hence #clipboard-tooltip never gets rendered. That is why tooltip is null.
Instead try this :
componentDidUpdate(prevProps, prevState) {
if(this.state.copied === true && prevState.copied === false) {
const tooltip = document.querySelector('#clipboard-tooltip')
tooltip.addEventListener('animationend', this.handleAnimationEnd)
}
if(this.state.copied === false && prevState.copied === true) {
const tooltip = document.querySelector('#clipboard-tooltip')
tooltip.removeEventListener('animationend', this.handleAnimationEnd)
}
}
componentDidUpdate gets called for every prop/state change and hence as soon as copied is set to true, the event handler is set inside componentDidUpdate. I have added a condition based on your requirement, so that it doesn't get executed everytime. Feel free to tweak it as needed.
I have a component that I can change how it is rendered based on a prop (added a failed state, and based on whether it fails or not it turns red or stays the original colour), the logic for whether failed is true or false is in the parent component.
I want to change the failed state, but only onBlur (without changing the child component). Is there a way to pass in an onBlur function which applies changes to a child prop?
Ive tried a number of different things like:
Child component
<input
failed={failed}
onBlur={onBlur}
/>
Parent component:
this.props.failed = value;
}
and in the render function:
onBlur={() => this.handleBlur(newValue)}
but it didnt work for me.
Props are data that are passed from a parent to its children and are made available through this.props in the child component.
You maintain whatever prop your are passing to child component either in parent component's state or in redux/flux state (if you have global state management).
When failed is modified, a state change should be triggered on parent component, which in-turn will trigger a re-render inside child component.
For example:
In the following, we pass failed as a prop, and onFailureUpdate function as a callback trigger to child component from parent.
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
failed: false
}
}
onFailureUpdate = (value) => {
this.setState({
failed: value
});
}
render() {
return (<ChildComponent failed={this.state.failed} onFailureUpdate={this.onFailureUpdate} />)
}
}
In child component, on blur, we are using the function we passed as prop to modify state in parent, which in-turn will re-render child component.
class ChildComponent extends React.Component {
onBlur = (e) => {
this.props.onFailureUpdate(e.target.value);
}
render() {
return (
<input
value={this.props.failed}
onBlur={(e) => this.onBlur(e)}
/>
)
}
}
Other way:
Or, if there's no necessity for props or parent-child relationship, you can eliminate the need for parent container and go for state maintenance in child.
class RewrittenChildComponentWithState extends React.Component {
constructor() {
this.state = {
failed: false
};
}
onBlur = (e) => {
this.setState({
failed: e.target.value
});
}
render() {
return (
<input
value={this.state.failed}
onBlur={(e) => this.onBlur(e)}
/>
)
}
}
Hope this solves your confusion.
I have multiple component with similar piece code in lifecycle methods and some similarity in state variables. Is there a way to unify them, by inheriting from one parent or something like that?
constructor(props) {
super(props);
this.state = {
//state properties similar in all components, getting from redux
//state properties specific for this component
}
// same code in many components
}
componentWillMount() {
// same code in many components
// code specific for this component
}
Can I use children methods and props in parent "wrapper" ? Can I change component state from parent ?
You can create Higher Order Component (HOC) for that, basically, you just write component with your same lifecycle method which is repeating, and then in render() function, call this.props.children function with any HOC internal state arguments you want, you can pass the whole state and a setState function as well, so you can change the HOC's state inside the underlying component.
For example:
class HOC extends React.Component {
constructor(props) {
super(props);
state = {
someState: 'foo',
};
}
componentWillMount() {
console.log('i mounted!')
}
render() {
return (
<div>
{this.props.children({ state: this.state, setState: this.setState })}
</div>
)
}
}
const SomeComponent = () =>
<HOC>
{({ state, setState }) => (
<div>
<span>someState value: </span>
<input
value={state.someState}
onChange={e => setState({ someState: e.target.value})}
/>
</div>
)}
</HOC>
You can also do really cool and interesting things with it, like connecting a slice of your redux state whenever you need it:
import { connect } from 'react-redux';
const ProfileState = connect(
state => ({ profile: state.profile }),
null,
)(({
profile,
children
}) => (
<div>
{children({ profile })}
</div>
));
const ProfilePage = () => (
<div>
Your name is:
<ProfileState>
{({ profile }) => (
<span>{profile.name}</span>
)}
</ProfileState>
</div>
);
Here is the full documentation on this technique.
You could create HOCs (Higher Order Components) in that case. It can look like this:
/*
A Higher Order Component is a function,
that takes a Component as Input and returns another Component.
Every Component that gets wrapped by this HOC
will receive `exampleProp`,`handleEvent`,
plus all other props that get passed in.
*/
function WithCommonLogic(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
example: ''
}
}
componentWillMount() {
...
// Same code in many components.
}
callback = () => {
/* Enhanced components can access this callback
via a prop called `handleEvent`
and thereby alter the state of their wrapper. */
this.setState({example: 'some val'})
}
render() {
return <WrappedComponent
exampleProp={this.state.example}
handleEvent={this.callback}
{...this.props}
/>
}
}
// You use it like this:
const EnhancedComponent1 = WithCommonLogic(SomeComponent);
const EnhancedComponent2 = WithCommonLogic(SomeOtherComponent);
Now all the shared logic goes into that HOC, which then wrap all your different components you want to share it with.
See the React Docs for further reading.
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).
How can I pass props to a component of a child page?
The prop that I am trying to pass is onToggleBooking: PropTypes.func which is defined in my layout.js (root file) as
lass Template extends React.Component {
constructor(props) {
super(props)
this.state = {
isBookingVisible: false,
}
this.handleToggleBooking = this.handleToggleBooking.bind(this)
}
handleToggleBooking() {
this.setState({
isBookingVisible: !this.state.isBookingVisible
})
}
render() {
const { children } = this.props
return (
<main className={`${this.state.isBookingVisible ? 'is-booking-visible' : ''}`}>
{children()}
</main>
)
}
}
Template.propTypes = {
children: PropTypes.func
}
export default Template
I want to pass onToggleBooking={this.handleToggleBooking} prop to {children()} so I am able to pass and use in a component of one of the child pages.
To do this I tried
{
children.map(child => React.cloneElement(child, {
onToggleBooking
}))
}
But I receive an error of children.map is not defined.
First, it's ideal to render children via props like:
render() {
return <div>{ this.props.children }</div>
}
Rendering children as prop functions is doable but you hould take a look to ensure your class the children are extended from is configured correctly, and the resulting render template is valid:
class CoolClass extends Component {
render() {
return this.props.children()
}
}
And then the template you call when rendering should look like:
<CoolClass>
{() => <h1>Hello World!</h1>}
</CoolClass>
You are close with passing sown the toggle handler using onToggleBooking={this.handleToggleBooking}, but it needs to be provided as a prop itself on a component or child your passing down. You can either edit the constructor to include it with your props.children, but that may be a pain to debug correctly calling children() prop as a fucntion.