How I think/understood was that react components update whenever their props or state change.
So I declare my variable:
let percentage = {
width: '10%',
};
and have a setInterval function running to change that variable after so long:
setInterval(function() {
percentage = {
width: '50%',
};
}, 5000);
and below this I render my component:
Meteor.startup(() => {
render((
<Router>
<div>
<Nav />
<Route exact path="/" render={() => <Start percentage={percentage} />} />
<Route path="/app" component={App} />
<Modal />
</div>
</Router>
), document.getElementById('render-target'));
});
Where I display the percentage in another file that looks like:
export default class Start extends Component {
render() {
return (
<div className="home">
<div className="meter orange">
<span style={this.props.percentage} />
</div>
</div>
);
}
}
My component never updates though, I have put a console.log into the setInterval function and get the update variable back but it never refreshes my component.
Have I misunderstood how props updates work?
The parameters passed to a component are copied by value, not reference. So when you render the outermost component, you're passing the current value of percentage into the Start component:
<Start percentage={percentage} />
From the perspective of the Start component, its property never changes, even though the variable that provided its initial value is.
You can't be clever and try to get around this with an object that contains a property percentage either...because the object (the parameter itself) won't change, only its properties.
So what's a poor programmer to do?
It's a bit misleading to say that a component updates when its properties change; components actually update when they're re-rendered. Very often, this happens because the enclosing (parent) component's state changes (or its been re-rendered) and it will be passing new props down to the inner component. The solution in your case is to make percentage part of the state of the enclosing component. So you would have something like this:
class Parent extends Component {
constructor(props, ...args) {
super(props, ...args)
this.state = { percentage: { width: '0%' } }
setInterval(() => this.setState({ percentage: { width: '50%' } }), 5000)
}
render() {
return <Start percentage={this.state.percentage} />
}
}
It's technically correct that a component updates when its props change; however, the only way to change its props is to re-render it! Props are read-only inside a component. Which is why I say it's misleading (or incomplete) to think about prop changes driving component re-rendering.
Related
I'm building a webpage and realized a common style shared by each component (same background, border, and title style). So I thought I should make an HOC which accepts the inner content of each component as well as a title, and returns an outer component which wraps this inner component and heading.
At first I ran into a lot of issues trying to get this to work, being new to React, but now it's finally working but I still don't understand how.
Here is my HOC
const BaseBlock = (WrappedComponent) => {
return class BaseBlock extends Component {
render () {
return (
<div className={styles['base-block']}>
<div className={styles['container']}>
<div className={styles['base-block-head']}>
{ this.props.title }
</div>
<div className={styles['base-block-body']}>
<WrappedComponent {...this.props} />
</div>
</div>
</div>
)
}
}
}
export default BaseBlock
This is the WrappedComponent:
const HighlightsBlock = (props) => {
return <ListsComponent items={props.items} />
}
export default BaseBlock(HighlightsBlock)
And this is the ListsComponent
const ListsComponent = (props) => {
if (props.items) {
return (
<ul className={styles['styled-list']}>
{props.items.map((item, idx) => {
return (
<li key={idx} className={styles['styled-list-item']}>{item}</li>
)
})}
</ul>
)
} else return (
<h3>No highlights</h3>
)
}
export default ListsComponent
And this is how I'm using the component in my app:
<HighlightsBlock items={this.getHighlights()} title='Highlights' />
Now, I can see the HighlightsBlock component receiving props twice (Once when I'm using it in my App with props, and once inside the HOC Baseblock as WrappedComponent ). If I remove props from either of these places it stops working. I don't understand how this is working.
When you render <HighlightsBlock items={this.getHighlights()} title='Highlights' /> you are actually rendering the component returned by HOC which in turn renders your actually HighlightsBlock component as <WrappedComponent {...this.props} />
You can think of HighlightsBlock component to be nested two level deep and hence you need to pass on the props to it, firstly as {...this.props} from within HOC and then receive it as props in functional component
This is because of this.getHighlights() in this line,
<HighlightsBlock items={this.getHighlights()} title='Highlights' />
Every time you pass props to child component this function is getting executed.
To solve this issue, maintain a state value in your parent component and set that value in getHighlights function like,
getHighlights(){
//you logic to get data
this.setState({items:data.items}); //considering `data` is object which has `items`
}
Now you can pass items like,
<HighlightsBlock items={this.state.items} title='Highlights' />
I created the following render props component, through the children prop as function:
export class GenericAbstractLoader extends React.Component<IGenericAbstractLoaderRenderProps, any> {
constructor(props: IGenericAbstractLoaderRenderProps) {
super(props);
}
public render() {
return this.props.isLoading ? (
<div className="generic-abstract-loader-wrapper">
<div className="generic-abstract-loader">
<img src={loader} />
</div>
{this.props.children(this.props)}
</div>
) : (
this.props.children(this.props)
);
}
}
It actually just renders the wrapped component if isLoading is false, or adds a layer of loading if the prop is true.
It works "quite perfectly".
I do call it in this way from another component:
public render() {
return (
<div>
<button onClick={this.justDisable}>JUST SET FALSE TO STATE</button>
<br />
<button onClick={this.enableDisable}>Disable/Enable Elements</button>
<br />
<GenericAbstractLoader customProp="custom prop" isLoading={this.state.shouldDisable}>
{props => <HomeTestForm {...props} />}
</GenericAbstractLoader>
</div>
);
}
As you can see the wrapped component is a simple HomeTestForm which contains just one input text:
export class HomeTestForm extends React.Component<IGenericAbstractLoaderHOCProps, any> {
constructor(props: IGenericAbstractLoaderHOCProps) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = { value: 'Initial Value' };
}
public render() {
return (
<div>
<input type="text" value={this.state.value} onChange={this.handleChange} />
</div>
);
}
public handleChange(event: any) {
this.setState({ value: event.target.value });
}
}
My issue is that when I toggle the isLoading prop, the state isn't kept by the wrapped component, so if I change the value of the inner text field, when adding the loading layer, and when removing it, the value is not taken, so the input field is still rendered with the init value.
What's the right way to create persistent wrapped components with render props?
Your component's state is getting reset because React is creating an entirely new component when your loading state changes. You can see these if you add a console.log to the constructor of HomeTestForm. React thinks this is necessary because of the ternary operator in GenericAbstractLoader. If you can restructure the render function in GenericAbstractLoader to something like the example below, you can give React the context it needs to persist the component instance across renders.
render()
return (
<div className={this.props.isLoading ? "generic-abstract-loader-wrapper" : ""}>
{this.props.isLoading && <img src={loader} />}
{this.props.children(this.props)}
</div>
);
}
React's documentation has a brief section about this under Reconciliation. This situation falls under the Elements of Different Types heading.
Also, unrelated to your question, based on your current example you don't need to use a render prop. The render function in GenericAbstractLoader can just render {this.props.children} without the function call and you can place your props directly on the child component as in the example below. You may have just simplified your example for SO and have a situation where you need render props, but I wanted to point this out just in case.
<GenericAbstractLoader isLoading={this.state.shouldDisable}>
<HomeTestForm customProp="custom prop" />
</GenericAbstractLoader
I'm having issues passing a prop to a componentDidMount() call in a child component on my React application.
In my App.js I am passing props via Router like below:
App.js
class App extends Component {
state = {
city: ""
}
componentDidMount() {
this.setState({city: this.props.city});
}
render() {
return (
<div>
<Route path="/" exact render = {() => <Projections city={this.state.city} />} />
<Route path="/:id" component={FullPage} />
</div>
);
}
}
In my Projections.js I have the following:
Projections.js
constructor(props) {
super(props);
this.state = {
location: this.props.city
}
}
componentDidMount () {
console.log(this.state.location);
console.log(this.props.city);
}
console.log(this.state);' returns an empty string.console.log(this.props.city);` returns an empty string as well.
But I need to access the value of the city prop within componentDidMount(). console.log(this.props.city); within render() returns the prop, but not in componentDidMount()
Why is this and how do I return props within componentDidMount()?
In the constructor you should reference props, not this.props:
location: props.city
<Route path="/" exact render = {() => <Projections city={this.state.city} {...this.props} />} />
Try passing rest of props in route
this is because you assigned props in constructor that time it may or may not receive actual value. And it gets called only once in a component lifecycle.
You can use componentWillReceiveProps to get props whenever it receive and update state accordingly.
Inside Projections.js
UNSAFE_componentWillReceiveProps(nextProps){
if(nextProps.city){
this.setState({location:nextProps.city})
}
}
Here is working codesand
So I have a global nav bar component that sits at the home screen and app screen and a music playing component. On click of one of the items in the nav bar I want to mute something on the music component.
Currently, to mute the music etc I'm using state.
So the way I've got this setup is to pass through an object as props and set that as state like so:
const obj = {
playing: false,
toggleButtonText: 'Play',
muteActive: false,
};
And I pass this as props into my components:
<Router>
<div>
<Nav stateVal={obj} />
<Route exact path="/" render={() => <Start />} />
<Route path="/app" render={() => <App stateVal={obj} />} />
<Modal />
</div>
</Router>
Then in each of my components, I do:
constructor(props) {
super(props);
this.state = this.props.stateVal;
}
So the props are set as the state of the component.
My problem is that I want one component to update the props and the update the state of the other component but I have no idea how I'm going to do that?
Could anyone give me a bit of help or pointers?
Assigning props to state in constructor is an anti-pattern because if the props change later on then the state isn't going to change.
Have the component update the props of the parent and then pass the props down the other child.
If you can't do this for some reason then you should look into Redux, Flux or MobX to handle the state.
Example
class Parent extends React.Component {
setMusicActive = (muteActive) => {
this.setState({ muteActive });
}
<ChildOne muteActive={this.state.muteActive} setMusicActive={this.setMuteActive} />
<ChildTwo muteActive={this.state.muteActive} setMusicActive={this.setMuteActive} />
}
class ChildOne extends React.Component {
someOtherFunction = () => {
this.props.setMuteActive(!this.props.muteActive);
}
}
Updates the value in one place and you can use it in the children.
I'm having trouble overcoming an issue with react router. The scenario is that i need to pass children routes a set of props from a state parent component and route.
what i would like to do is pass childRouteA its propsA, and pass childRouteB its propsB. However, the only way i can figure out how to do this is to pass RouteHandler both propsA and propsB which means every child route gets every child prop regardless of whether its relevant. this isnt a blocking issue at the moment, but i can see a time when i'd be using the two of the same component which means that keys on propA will overwritten by the keys by the keys of propB.
# routes
routes = (
<Route name='filter' handler={ Parent } >
<Route name='price' handler={ Child1 } />
<Route name='time' handler={ Child2 } />
</Route>
)
# Parent component
render: ->
<div>
<RouteHandler {...#allProps()} />
</div>
timeProps: ->
foo: 'bar'
priceProps: ->
baz: 'qux'
# assign = require 'object-assign'
allProps: ->
assign {}, timeProps(), priceProps()
This actually works the way i expect it to. When i link to /filters/time i get the Child2 component rendered. when i go to /filters/price i get the Child1 component rendered. the issue is that by doing this process, Child1 and Child2 are both passed allProps() even though they only need price and time props, respectively. This can become an issue if those two components have an identical prop name and in general is just not a good practice to bloat components with unneeded props (as there are more than 2 children in my actual case).
so in summary, is there a way to pass the RouteHandler timeProps when i go to the time route (filters/time) and only pass priceProps to RouteHandler when i go to the price route (filters/price) and avoid passing all props to all children routes?
I ran into a similar issue and discovered that you can access props set on the Route through this.props.route in your route component. Knowing this, I organized my components like this:
index.js
React.render((
<Router history={new HashHistory()}>
<Route component={App}>
<Route
path="/hello"
name="hello"
component={views.HelloView}
fruits={['orange', 'banana', 'grape']}
/>
</Route>
</Router>
), document.getElementById('app'));
App.js
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return <div>{this.props.children}</div>;
}
}
HelloView.js
class HelloView extends React.Component {
constructor(props) {
super(props);
}
render() {
return <div>
<ul>
{this.props.route.fruits.map(fruit =>
<li key={fruit}>{fruit}</li>
)}
</ul>
</div>;
}
}
This is using react-router v1.0-beta3. Hope this helps!
Ok, now that I'm understanding your issue better, here's what you could try.
Since your child props are coming from a single parent, your parent component, not react-router, should be the one managing which child gets rendered so that you can control which props are passed.
You could try changing your route to use a param, then inspect that param in your parent component to render the appropriate child component.
Route
<Route name="filter" path="filter/:name" handler={Parent} />
Parent Component
render: function () {
if (this.props.params.name === 'price') {
return <Child1 {...this.getPriceProps()} />
} else if (this.props.params.name === 'time') {
return <Child2 {...this.getTimeProps()} />
} else {
// something else
}
}
In child component, insted of
return <div>{this.props.children}</div>
You may merge props with parent
var childrenWithProps = React.cloneElement(this.props.children, this.props);
return <div>{childrenWithProps}</div>
React.cloneElement can be used to render the child component and so as pass any data which is available inside the child route component which is defined in the route.
For eg, here I am passing the value of user to the react childRoute component.
{React.cloneElement(this.props.childRoute, { user: this.props.user })}