As you want to build complex component, it would be great if you can wrap any DOM with component such as "lazy-load" component with condition (#Prop() condition: boolean) so to illustrate what I want:
<lazy-load condition={some boolean condition, like certain link get clicked and section is now active}>
<data-fetch>
</data-fetch>
</lazy-load>
in this example, "data-fetch" will make a HTTP call to grab some large data, and I want to defer this component added to DOM until condition we specify in the lazy-load component to be true.
So I started to implement render() of lazy-load component as something along the line of
#Prop() condition: boolean;
render() {
if(!this.condition) {
return null;
}
return (
<slot/>
);
}
and try to use it as
<lazy-load condition={false}>
<data-fetch>
</data-fetch>
</lazy-load>
but no matter what I tried, data-fetch component get added to DOM (and while we can set visibility to hide element, we would waste HTTP call) I understand I can put the same condition in the data-fetch itself and then not make a fetch call when condition is false, but if possible I want generic wrapper component to achieve this (if you are familiar with AngularJS or Angular, I want to find a way to do equivalent of ng-if and *ngIf off of generic wrapper component)
Maybe this is a limitation due to how "slot" tag supposed to work? (also, I'm using it with #Component({shadow: false}) so I know I'm not using standard shadowDOM from the web component spec so maybe what I'm trying to do is not feasible?
Thank you very much for your time in advance to even read this question and I appreciate any help I can get. I feel if we can do this, we might be able to build component that can quickly differ loading until whenever we feel it should load/render.
Yeah it's an issue with not using shadow: true, because in the polyfill the slotted content just becomes part of the light DOM (but gets placed where the slot element is). Beware that even if you enable Shadow DOM, it'll still fallback to the polyfill if the browser doesn't support it. You could raise an issue about this in Github but I'm not sure if/how it would be possible to solve this "dynamic slot" problem.
But I think you can take a simpler approach:
{myCondition && <data-fetch />}
That way the data-fetch element will only be added once the condition becomes true.
You could also refactor this into a functional component:
import { FunctionalComponent } from '#stencil/core';
interface Props {
if: boolean;
}
export const LazyLoad: FunctionalComponent<Props> = ({ if }, children) =>
if && children;
import { LazyLoad } from './LazyLoad';
<LazyLoad if={condition}>
<data-fetch />
</LazyLoad>
Related
From React documentation.
Conceptually, components are like JavaScript functions. They accept
arbitrary inputs (called “props”) and return React elements describing
what should appear on the screen.
Considering:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
or
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
Will give us the ability to do this:
<Welcome name="Luke" />;
<Welcome name="Leia" />;
to use as we wish in the DOM,
Hello, Luke
Hello, Leia
Now when people prescribe props shouldn't be changed, it would make sense the reason is in my thinking would be like the same as changing the values of attributes of an image tag?
HTML:
<img id="Executor" alt="Picture of Executor" src="/somepath/vaders-star-destroyer-executor.jpg"/>
JS:
Meanwhile in a Javascript file a long time ago in a galaxy far, far away...
var imageOfVadersStarDestroyer = document.getElementById('Executor');
imageOfVadersStarDestroyer.src = "/somepath/vaders-star-destroyer-avenger.jpg"
Because if we keeping changing an elements attribute values this can cause confusion and slower renderings?
So is the reason why the prescription is to never change props in React is because is the library is trying to make elements as predictable as possible?
Setting props outside of React is dangerous and should be avoided. Why? The main reason is that it doesn't trigger re-renders. Hence bugs and unexpected behaviour.
Re-rendering
Most of the time, props are data that is store as state in the parent component, which is manipulated by calling setState() (or the second function returned by React.useState()). Once setState() is called, React re-renders and computes what has changed under the hood, with the latest props and state. Manually assigning values to props, therefore won't notify React that the data has changed and something has to be re-rendered.
The good practice
Making props read-only allows React components to be as pure as possible, which is obviously a good practice anyway even when writing plain JS. Data won't be changed unexpectedly and can only be done so by calling setState() (You might have heard of the single source of truth, which is what React is trying to leverage).
Imagine you notice something went wrong in the app and the data shown to the end user is completely different from the source, it would be a pain trying to find out where the data has been manipulated wouldn't it? :)
never change props in React
means that you should never do this.props.name = "userName" because of React's one way data binding, props are read only, to update a component's props, you should pass a function from the parent that will do that ( in the parent ) , or dispatch an action if you're using redux, a change in the props will trigger a re-render
props is a constant in this case. You will always need it in your components.
But there is a cleaner way to write it or even omit it.
Regular way with Function Expression (same as your exemple)
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
ES6 Object Destructing - explicit
function Welcome(props) {
const {name} = pros
return <h1>Hello, {name}</h1>;
}
ES6 Object Destructing - inplicit, cleaner way
function Welcome({name}) {
return <h1>Hello, {name}</h1>;
}
And of course, you can use the class way which requires the usage of this.props.yourAttr
However, in the new version 3 of create-react-app, changed class components to functional components. You can see this exact modification on Github here.
You can need to learn more about destructing assignment in the old and good MDN linked here or an in-depth approach both array and object destructuring here.
So I have a button that lives in a container and uses a callback on it's onClick event to update state in the container, fairly basic stuff. But now I want that container to let a different child know that the button was clicked so it can trigger the appropriate response. (and it should only let the child know once so the response isn't being triggered a million times)
The way I solved this looks and feels and quacks like a code smell, so I thought I'd ask you guys if there is a better way to do it. Here is what I did:
class myContainer extend Component {
constructor(){
super()
state= { triggered: false }
}
componentWillUpdate(nextProps, nextState){
this.hasTriggered = this.state.triggered !== nextState.triggered
}
triggerResponse = () => this.setState({...this.state, !this.state.triggered})
render(){
return (
<myButton onClick={triggerResponse}/>
<myComponent hasTriggered={this.hasTriggered}/>
)
}
}
Now this seems to work perfectly fine, and maybe this is what I should do, but it just feels like there has to be a neater way of sending a simple message of "I have been clicked" to a component in the same container.
One major red flag for me is that "triggered" is a boolean, but it doesn't matter if it is true or false, so if triggered is false, it means nothing, all that matters if it was the other boolean last round. This seems like a violation of good practices to me.
*Summary: What I'm looking for is a snappy way to give state a value for just one update cycle and then go back to null or false without having to update it again. Or a different way to get the same result.
I came up with 2 different yet unsatisfying answers:
class myContainer extend Component {
constructor(){
super()
state= { hasTriggered: false }
}
shouldComponentUpdate(nextProps, nextState){
return (!nextState.hasTriggered && this.state.hasTriggered)
}
componentDidUpdate(nextProps, nextState){
if(nextState.hasTriggered)this.setState({hasTriggered: false})
}
triggerResponse = () => this.setState({hasTriggered: true})
render(){
return (
<myButton onClick={triggerResponse}/>
<myComponent hasTriggered={this.state.hasTriggered}/>
)
}
}
This is unsatisfying because it is a lot of code for a very simple button click. I am setting state, sending it down, resetting state and then ignoring the next render call all to accommodate a lousy button click. The good news is that I'm no longer misusing a boolean, but this is definitely too much code.
class myContainer extend Component {
componentDidUpdate(){
this.hasTriggered = false
}
triggerResponse = () => {
this.hasTriggered = true
this.forceUpdate()
}
render(){
return (
<myButton onClick={triggerResponse}/>
<myComponent hasTriggered={this.hasTriggered}/>
)
}
}
I find this method unsatisfying because I no longer have the shared state in state. Before I also did this by comparing new and old state and making a variable carry the result over to my component, but at least then I could look at my state and see that there is a state variable that has to do with this button. Now there is local state in my component that is in no way linked to the actual state, making it harder to keep track of.
After thinking about this all day I've come to the conclusion that #ShubhamKhatri was on the right track in his comment, I thought I was pulling up state to my container by using a callback and passing state down, but clearly there is too much logic being executed in my component if it's handling a click event. So my new rule is that in this kind of scenario you should just pull up whatever state you need to execute the onClick inside the container. If your dumb components are executing anything other than a callback it is a mistake.
The reason I was tempted to do the onClick logic in my presentational component is because I was using a third party library(d3) to handle the graphics and so I didn't consider that the state I wasn't pulling up was the d3 state, if I move that up, change it when the button is clicked and then pass it down to my component it works beautifully.
Now this means I need to import d3 in two places and I did have to write a bit more code than before, but I think the separation of concerns and the overall cleanliness of my code is well worth it. Also it made my child component a lot easier to maintain, so that's nice.
I want to use react-id-swiper library which exports a component named Swiper.
This is its render method:
render() {
const { containerClass, wrapperClass, children, rtl } = this.props;
const rtlProp = rtl ? { dir: 'rtl' } : {};
return (
<div className={containerClass} {...rtlProp}>
{this.renderParallax()}
<div className={wrapperClass}>
{React.Children.map(children, this.renderContent)}
</div>
{this.renderPagination()}
{this.renderScrollBar()}
{this.renderNextButton()}
{this.renderPrevButton()}
</div>
);
}
}
This component perfectly matches my needs, except that I need to place pagination in an outer place (outside of the containerClass element).
One possible solution is to inherit Swiper class and change only it's render method. However Facebook docs are explicit to not use inheritance and use composition instead.
What's the best way to move the pagination outside of containerClass?
Can it be done with composition?
As it stands there is not much you can do, since this component is render specifically in this structure align with a secondary library. In the rebuildSwiper function you can see it passing it the dom elements to this other library to bind and handle all interactions.
Depending on the flexibility of the secondary app you could move the render order. To do so I would first evaluate what the capabilities are of the inner library, then fork this repo and publish updates using the a scoped package such as #<username>/react-id-swiper.
I think composition would be well and good, but because you're using a library and not able to design the source class, I think inheritance is your only option.
I would recommend simply extending the Swiper class. You can write your new render method in a way that would allow for composition reuse moving forard if you want to follow these recommended practices, but I think it's safe to trust your gut in this case.
I'll be watching this question and am curious to see how others would approach it and what you're ultimate take is, too.
I'm using react-bootstrap tooltip to build a react component. According to react-boostrap specifications, I need to wrap my element with an OverlayTrigger element, like this:
<OverlayTrigger overlay={this.getTooltipElement()} placement='left'>
//element
</OverlayTrigger>
This works just fine, but my component requires to show the tooltip when hovered (usual behavior) AND only if a boolean variable is set to true. I've tried this:
if (booleanParameter) {
return (<OverlayTrigger overlay={this.getTooltipElement()} placement='left'>
{myElementVariable};
</OverlayTrigger>)
} else {
return myElementVariable;
}
But when the tooltip is instructed to show (by the boolean parameter), I get the following warning:
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the undefined component.
Important: getTooltipElement() uses component state fields.
... and the component starts to act erratically. How should I go to solve my problem? How can I wrap and element depending on a boolean parameter? Is there another, less elaborated solution?
I believe the approach you are using is fine, the warning essentially tells you that somewhere - possibly in whatever getTooltipElement() returns, you're calling setState after the component unmounted. You fix this by checking whether the component is still mounted before you set the state:
if(this.isMounted())
this.setState({...});
By the way, if you give your components a 'displayName' attribute, the warning will actually tell you in which component it happened, making it easier to find.
I have a situation which isn't too contrived, and I'm having trouble implementing it using the React best practices. In particular it produces this error:
Uncaught Error: Invariant Violation: setProps(...): You called setProps on a component with a parent. This is an anti-pattern since props will get reactively updated when rendered. Instead, change the owner's render method to pass the correct value as props to the component where it is created.
The situation is like this. The parent contains a child component. The parent has event handlers for UI and for the behavior to work, something inside the child component needs to render its HTML with a CSS change to the height style. Therein lies the wrinkle, usually the information flows upward or stays put, but here I need to change something in the child.
Parent component (Widget) renders this:
<div class="Widget">
<div class="WidgetGrabBar" onMouseDown={this.handleMouseDown}>
<WidgetDetails heightProp={this.props.detailsHeight} />
</div>
And elsewhere in Widget I've got
componentDidMount: function() {
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
},
componentDidUnmount: function() {
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
},
<...>
handleMouseDown: function(e) {
e.preventDefault();
this.props.actuallyDragging = true;
},
handleMouseUp: function(e) {
this.props.actuallyDragging = false;
},
handleMouseMove: function(e) {
e.preventDefault();
if (this.props.actuallyDragging) {
// update the prop! I need to send an urgent package of information to my child!! jQuery or findDOMElement() followed by DOM traversal is forbidden!!!
this.setProps({
detailsHeight: this.props.detailsHeight + e.deltaY
});
}
},
And I had WidgetDetails' render() render something like:
<div class="WidgetDetails" style={height: this.props.heightProp}>
{detail_items_move_along_nothing_to_see_here}
</div>
I figured that rolling out the jQuery to grab .WidgetDetails to fiddle with its style attr is the wrong thing, the non-React way to go about it. The real anti-pattern.
But now I'm being told that I can't change my props. Or I have to throw out everything including the bathwater in order to have new props. I'm not doing that; my props contain the contents of the detail items. Maybe it is expensive to make another entirely new copy of this.
I'm trying to let React participate in this rendering work to put the new height in. How am I supposed to even do this? Is this error basically enforcing that Props are supposed to be immutable now? The error is telling me that I have to involve this height even farther up on the component chain. I can conceivably do so with a callback from up above, but this feels very wrong. I need to pass information downward, not upward.
Maybe I'm supposed to use state. But changing state forces Widget, the parent component to render. That is not what I desire. Only one singular place in the DOM needs to re-render, that is the child component's div's style attr.
There are two approaches. Either
call handlers on the parent. Then Pass the new props to the child via props. If I recall correctly, that's the approach the react hello world tutorial takes.
Mutate state in the view via setState.
In your case, it seems that approach 2 really makes sense. You are basically trying to track view data.
Never, by the way, update state directly. Use setState. The whole point of reacts virtual dom is that it's optimized for spurious updates, so you will be fine. There is also the life cycle method componentShouldUpdate in case you want finer control.
For completeness I should add that there's a third way of using a global store. That's what react flux adds. But again, in your case that's probably over kill.