I've built in the past a "single page application" using nothing besides regular DOM manipulations with JQuery.
Nowadays, I'm rewriting the same app with React-Redux. After re-creating a certain page from my old JQuery app, I've noticed something disturbing: React does the same thing, MUCH MUCH slower. In fact, I'd say it's about twice as slow.
I started optimizing my component, and made sure it renders only when necessary. Yet, the entire process of re-rendering the HTML is slower.
What the component does: i fetch an array of 150 items from the server. Each item might have few items in it. So, my JSX has two nested map() functions:
{this.props.duplicates.length>0 &&(
this.props.duplicates.map((duplicate_group,index)=>{
return(
<form key={index}>
{
duplicate_group.map((book)=>{
return(
<div key={book.id}>
<label className="specific_product_for_merging" htmlFor={book.id} id={book.id}>
<input type="radio" defaultValue={book.id} id={book.id} name="name" />
<span className="duplicates_span">Title:<Link to={`/products/${book.id}`}> {book.name}</Link> </span>
<span className="duplicates_span">ID: {book.id}</span>
<span className=" duplicates_span">Author:<Link to={`/advancedsearch?advancedSearch=byTitle&author=${book.author}`}> {book.author} </Link></span>
<span className=" duplicates_span">Publisher:<Link to={`/advancedsearch?advancedSearch=byTitle&publisher=${book.publisher}`}> {book.publisher}</Link></span>
<span className="duplicates_span">Available Copies: 0</span>
</label>
<br />
</div>
)
})
}
<input type="submit" className="submit_merged_titles btn btn-primary btn-sm" defaultValue="Merge" />
</form>
)
})
)
}
I was doing the same exact thing with Jquery. For some reason, the direct DOM manipulation seems to work much faster, in this case at least.
Note that each time i fetch new data(i'm using pagination), all of the data is different, so React has to re-render all DOM elements. But how come the entire process, of re-rendering the virtual DOM, and then rendering the DOM itself, is practically twice as slow?
Related
I'm trying to build some simple stuff with Astro.js for HTML templates and Alpine.js for interactivity when needed. I stumbled upon problem of trying to use Alpine x-on events on Astro component
Here is sample usage of the Button component:
<div x-data>
<Button #click="console.log('Clicked astro')">Click me</Button>
<button #click="console.log('Clicked normal button')">Click me</button>
</div>
The first one doesn't work while the native button works as normal.
Button component is just a button element with some tailwind classes, nothing fancy:
<button class="border-[1px] rounded-lg transition px-6 py-2 hover:text-inherit hover:border-transparent">
<slot />
</button>
How can I get it to work? I really love astro with alpine so far but this little thing is stopping me from using it for my project.
I tried various of different things but none of them worked really. I went through docs and couldn't find anything related to my issue - it feels like there must a simple solution but I'm missing something.
I have reached out to Astro support on discord and it was answered by user happydev there:
Anything passed to an astro component like this is taken as a prop, so you need to pass it to the element you want to bind the event on
Something like this
//Button.astro
---
const {"#click": click} = Astro.props
---
<button #click={click}>
<slot/>
</button>
This is kind of an example scenario what the problem looks like,
<div x-data="{ count : 0 }">
<div x-data>
<span x-text="count"></span>
<button x-on:click="count++">Increment</button>
<button x-on:click="count--">Decrement</button>
</div>
</div>
It would be able to increase/decrease the data count from the child component.
I thought of handling it through dispatching custom events using $dispatch() but then again in terms of design, I might need to write listeners on both parent and child component which make the logic more complex since it should be reactive as well.
There was a Github issue, and none of the proposed solutions was working.
I thought of handling it through dispatching custom events using $dispatch() but then again in terms of design, I might need to write listeners on both parent and child component which make the logic more complex since it should be reactive as well.
This is the crux of the issue, in order to do parent-child and child-parent communication you'll need to use events. In the case of child -> parent, you'll trigger increment and decrement events (which will be listened to in the parent component using x-on:increment and x-on:decrement). In the case of parent -> child, you'll need to use $watch to trigger updates whenever count updates (I'll used the new-count event name), this will be listened to on the window from the child component using x-on:new-count.window.
Here's the full working solution (see it as a CodePen):
<div
x-data="{ count : 0 }"
x-init="$watch('count', val => $dispatch('new-count', val))"
x-on:increment="count++"
x-on:decrement="count--"
>
<div>In root component: <span x-text="count"></span></div>
<div
x-data="{ count: 0 }"
x-on:new-count.window="count = $event.detail"
>
<div>In nested component <span x-text="count"></span></div>
<button x-on:click="$dispatch('increment')">Increment</button>
<button x-on:click="$dispatch('decrement')">Decrement</button>
</div>
</div>
In the case you've presented, the count state might be better served by using a global store that integrates with Alpine.js such as Spruce, in which case we'll read and update a shared global store to which both the parent and child components are subscribed (see the Spruce docs). You can find the working example in the following CodePen.
<div x-data x-subscribe="count">
<div>In root component: <span x-text="$store.count"></span></div>
<div x-data x-subscribe="count">
<div>In nested component <span x-text="$store.count"></span></div>
<button x-on:click="$store.count ++">Increment</button>
<button x-on:click="$store.count--">Decrement</button>
</div>
</div>
<script>
Spruce.store('count', 0);
</script>
The final solution that should be mentioned is that removing the nested component would mean that the count increment and decrement would work as expected. Obviously this example was probably simplified & meant to be illustrative, so this solution might not work in a lot of cases. Note: the only difference is removing the second x-data.
<div x-data="{ count : 0 }">
<div>
<span x-text="count"></span>
<button x-on:click="count++">Increment</button>
<button x-on:click="count--">Decrement</button>
</div>
</div>
Another way is to use the magic $store 👉as a Global State:
<div x-data x-init={Alpine.store('count', 0)}>
<div x-data>
<span x-text="$store.count"></span>
<button x-on:click="$store.count++">Increment</button>
<button x-on:click="$store.count--">Decrement</button>
</div>
</div>
There is a "portal" pattern which specifies that an element should be appended to the document.body. I want something similar to this, however, it must append to a specific component. My basic layout component would look as follows (Codepen Here):
<div className={styles.container}>
<div className={styles.header}>
<Navbar />
</div>
{/* Here be the problem */}
<PortalTarget />
<div className={styles.content}>
<div className={styles.leftSidebar}>
<SidebarNav />
</div>
<div className={styles.main}>
{children}
</div>
</div>
</div>
Instead of rendering to document.body, it should find the PortalTarget element (ideally only one would be rendered) and then append the <Portal> into that DOM Node.
I am specifically having trouble creating and storing a reference to the <PortalTarget>, I can't seem to find a good way to do it without redux.
Is there a way to do this in React?
If using an external library issue not a concern, just type "react portal" or "react gateway" on any search engine.
There a few battle-tested libraries out there.
I am trying to learn React. I already have a good grasp of javascript. I am trying to learn by creating a small app that is basically a task manager. In my case it's for grocery related items. I have a fiddle created here. Could you please take a look at how I composed the react code and let me know if this is the best approach to building with components/classes? You can see that I have nested components. I am not sure if there is a better way of doing this.
Finally, I wan't a new "add-item-row" created every time a user clicks on the big blue Add button. Right now one is showing be default but I don't want any showing by default. I want one created (add-item-row, div) only when a user clicks on the Add button.
Here is the fiddle.
https://jsfiddle.net/j0mpsbh9/4/
<div id="app" class="container">
<script type="text/babel">
var AddItemWrapper = React.createClass({
render: function() {
return (
<div>
<div className="row">
<AppTitle />
<AddItemForm />
</div>
<AddItemRow />
</div>
);
}
});
var AppTitle = React.createClass({
render: function() {
return (
<div>
<h1>Grocery List</h1>
</div>
);
}
});
var AddItemForm =React.createClass({
render: function() {
return (
<div>
<div className="col-sm-6 col-lg-6">
<div className="form-group">
<label htmlFor="enter-grocery-item" className="sr-only">Enter Grocery Item</label>
<input type="text" className="form-control" id="enter-grocery-item" placeholder="Enter Grocery Item" />
</div>
</div>
<div className="col-sm-6 col-lg-6">
<button type="button" id="add" className="btn btn-block btn-info">Add <span className="glyphicons circle_plus"></span></button>
</div>
</div>
);
}
});
var AddItemRow =React.createClass({
render: function() {
return (
<div className="add-item-row">
<div className="row">
<div className="col-sm-12 grocery-items">
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="grocery-item" className="sr-only">Grocery Item</label>
<input type="text" className="form-control" id="grocery-item" placeholder="" />
</div>
</div>
<div className="col-sm-6 center">
<button type="button" className="btn btn-blockx btn-warning"><span className="glyphicons pencil"></span></button>
<button type="button" className="btn btn-blockx btn-lgx btn-danger"><span className="glyphicons remove"></span></button>
<button type="button" className="btn btn-blockx btn-lgx btn-success"><span className="glyphicons thumbs_up"></span></button>
</div>
</div>
</div>
</div>
);
}
});
ReactDOM.render(
<AddItemWrapper />,
document.getElementById('app')
);
</script>
</div>
You have a good start here! Your component hierarchy is organized in a sensible way. However you are missing any kind of interactivity or internal state.
The main way you make React components interactive is by using state plus event callbacks which modify said state. "State" is pretty self explanatory - it describes values inherent to how the components looks and behaves, but which change over time. Every time a React component's state is altered (with this.setState()) that component will re-render (literally by re-running the render() function) to reflect the changes.
First let's edit your AddItemWrapper class to keep track of some internal state when it is first mounted. We know that you want to have multiple rows of data, so let's give it an empty array to store future information about rows:
getInitialState(){
return {rows: []};
},
Now instead of rendering a single AddItemRow directly, we'll render a dynamic set of rows that is based on the current component state. Array.map() is perfect for this and a common use case in React:
{this.state.rows.map(function(ea, i){
return <AddItemRow initialItemName={ea} key={ea + "-" + i} />
})}
Basically what that does is take every entry in the array AddItemWrapper.state.rows array and renders it as an AddItemRow component. We give it two properties, initialItemName and key. "initialItemName" will just tell the child component what its name was when it was first added, and "key" is a unique string that allows React to differentiate components from their siblings.
Now we've set up AddItemWrapper to properly render rows based on its internal state. Next we have to modify AddItemForm so that it will react to user input and trigger new rows being added.
In AddItemForm, first we need to add a "ref" to the input text box. This is so that React can identify and read data from this HTML element after it is rendered:
<input ref={function(el){this.inputElement = el;}.bind(this)} ... />
Then give the button element a callback that will trigger when it's clicked:
<button onClick={this.handleClick} ... />
Finally write the callback handler itself:
handleClick(){
this.props.onAdd(this.inputElement.value);
this.inputElement.value = "";
}
Notice how this callback is calling this.props.onAdd()? That means we need to pass in a callback function from the parent (AddItemWrapper) to this component to use. This is how we communicate between parents and children in React: pass a function from a parent to a child which will be triggered from within the child, but will effect the parent.
In AddItemWrapper we make sure AddItemForm has access to the callback function:
AddItemForm onAdd={this.onAdd} />
And then we write the callback function itself:
onAdd(newItem){
var newRows = this.state.rows.slice();
newRows.push(newItem);
this.setState({rows: newRows});
}
Notice how we're copying the old array held in state (using Array.slice()), push a new item into the new array, and then update state with the new array? Never mutate state directly; ALWAYS copy it, modify the copy, and then update state with the new copy.
Almost done. We've created a way for AddItemWrapper to render its rows, and a way for AddItemForm to create new rows. Now we edit AddItemRow to render in a way that maintains its own internal state too.
First make sure it initializes its own state when it's mounted. We'll have it keep track of a string value, which initially is the same as what the user entered into the text box when they pressed "Add", but because it's kept in AddItemRow.state it can be modified later by the user:
getInitialState() {
return {itemName: this.props.initialItemName}
}
Now that the row name is kept in the component state, we can render it in the HTML like this:
<input value={this.state.itemName} ... />
Here's what it looks like when you put it all together!
There are obviously more features that you would want to add, such as letting the user edit, move, or delete a row entry. I'll leave those exercises up to you. I highly recommend you read through all of the official documentation as well as do a few tutorials to get your head in the game. It's obvious that you have a bit of experience under your belt given what you had so far, but getting the hang of how state, render(), and callbacks work takes some practice. Good luck!
I want to achieve the following DOM mutation using ReactJS, and animate it so that it cannot be noticeable by the end user.
<RootComponent>
<MyComponent id="a"/>
<div>
<MyComponent id="b"/>
</div>
<MyComponent id="c"/>
</RootComponent>
to
<RootComponent>
<MyComponent id="a2"/>
<div>
<MyComponent id="a"/>
</div>
<MyComponent id="c"/>
</RootComponent>
I successfully handle the animation (by moving the DOM Node around through the parent) and replace b with a, along with creating a new a2component.
The problem is, as soon as I ask React to rerender the root component (by placing a2), I run into an invariant violation. Indeed, I mutated the DOM manually, and data-reactids might be shuffled.
How could I complete such a workflow following React's best practices ?