How to force an update of a subcomponent? - javascript

Lit Documentation says:
When called with no arguments, requestUpdate() schedules an update,
without calling a hasChanged() function. But note that
requestUpdate() only causes the current component to update. That
is, if a component uses the code shown above, and the component
passes this.myArray to a subcomponent, the subcomponent will detect
that the array reference hasn't changed, so it won't update.
So how to force an update of a subcomponent (even if the subcomponent's attribute doesn't change)?
render() {
return html`
<ul>
${this.myArray.map(
(item) => html`
<li>
${item.text}
<my-subcomponent foo="${item.bar}"></my-subcomponent>
</li>
`
)}
</ul>
`;
}
Edit: Repro in Lit Playground

Thank you for the lit.dev playground repro, I was able to identify some issues that should resolve your issue.
Before jumping into the various fixes, why is the subcomponent not re-rendering? LitElements render as a function of their properties & state. If properties don't change, they don't re-render.
The subcomponent has two reactive properties, myArray and id. myArray is only set once on construction from localstorage and never updated - so it will never trigger an update. The id number also never changes for a given subcomponent so will never trigger an update. Because neither properties change, the subcomponent doesn't re-render.
Option 1: Pass the new array to the sub-component.
Fix 1 in Lit Playground
The only change I've made is to pass myArray to sub-component explicitly, (note .myArray=${this.myArray}):
<li>${item.text} (<sub-component id=${item.id} .myArray=${this.myArray}></sub-component>)</li>
This works because now when the parent updates this.myArray, the parent passes this new array to the sub-component. The sub-component then notices that the reactive property myArray has changed and a render is scheduled.
Option 2: Only pass the item to the sub-component
This is a larger change, but more maintainable and simpler. Instead of passing both the array and item id to each sub-component, only pass the item that the sub-component cares about.
This does less work on the sub-component because each sub-component doesn't need to loop through the whole array to find the element that it cares about.
Working fixed playground from repro.
This approach requires a change in changeMyArray. Instead of mutating the items, new items need to be returned.
changeMyArray() {
this.myArray = this.myArray.map(
(item) => {
return {...item, colored: !item.colored};
}
);
}
Breaking down that example. this.myArray.map already returns a new array so the array spread is not required. Within the map each item that is modified must return a new object. This is similar to how redux works and the linked article may provide more helpful details.
Now the parent render function can be updated to pass the item directly to the subcomponent with: <li>${item.text} (<sub-component .item=${item}></sub-component>)</li>.
Now when the item changes, the sub-component automatically re-renders.
Option 3: Manually calling a method that triggers an update
I would not recommend this over option 2, but wanted to show how you could trigger the sub-component to re-render.
Working sample: https://lit.dev/playground/#gist=fb38e52bc4d35dd74485407eb19db84f
There are two changes. In the parent, 'main-component', changeMyArray method I've added a loop that calls a method on each sub-component:
this.shadowRoot.querySelectorAll('sub-component')
.forEach(component => component.refresh())
And on the sub-component I've defined refresh as:
refresh() {
this.myArray = JSON.parse(localStorage.getItem('myArray'));
}
Because the sub-component now has the new version of this.myArray from localstorage it will trigger a re-render.
The cons of this approach and why option 1 or 2 is better:
Each sub-component needs to manually parse the array out of localstorage.
This approach requires manually managing when renders happen instead of letting the state flow down through properties.
There's a video called Event communication between web-components by Elliott on the Lit team that discusses state and events further.

Related

Alter react component state properly

I'm working at a project in which I have to display graphs.
For displaying graphs I'm using vis.js in particular react-vis-network a implementation for using parts of vis.js in React with its stateful approaches.
Initial nodes and edges are loaded before my component is mounted and are passed as props for an initial state.
I attached two eventHandler one direct to a vis.js (the underlying DOM library) and the other at a decorator (button).
The desired/expected behaviour:
A node is removed by clicking either the node or the corresponding button.
Observed behavior:
Sometimes a node is removed and sometimes a node just disappears for a few ms and is reattached but without a decorator/button.
I already tried to start with an empty state and attaching the nodes,edges in componentDidMount() but I got the same result. I hope you can give me a hint.
BTW: Is the way I use to attach components a/the right way?
Every other help to improve my class is appreciated also
class MyNetwork extends Component {
constructor(props){
super(props);
let componentNodes = [];
for (let node of props.nodes){
componentNodes.push(this.createNode(node));
}
let componentEdges = [];
for (let edge of props.edges){
componentEdges.push(this.createEdge(edge));
}
this.state = {nodes:componentNodes,edges:componentEdges};
["_handleButtonClick"].forEach(name => {
this[name] = this[name].bind(this);
});
}
createNode(node){
const Decorator = props => {
return (
<button
onClick={() =>{this._handleButtonClick(props);}}
>
Click Me
</button>
);
};
node.decorator = Decorator;
return React.createElement(Node,{...node})
}
createEdge(edge){
return React.createElement(Edge,{...edge})
}
addNode(node){
this.setState({
nodes: [...this.state.nodes, this.createNode(node)]
})
}
_handleButtonClick(e) {
if(e){
console.log("clicked node has id:" +e.id);
this.removeNode(e.id);
}
}
onSelectNode(params){
console.log(params);
window.myApp.removeNode(params[0]);
}
removeNode(id) {
let array = [...this.state.nodes]; // make a separate copy of the array
let index = array.findIndex(i => i.props.id === id );
array.splice(index, 1);
this.setState({nodes: array});
}
render() {
return (
<div id='network'>
<Network options={this.props.options} onSelectNode={this.onSelectNode}>
{[this.state.nodes]}
{[this.state.edges]}
</Network>
</div>
);
}
}
export default MyNetwork
Before clicking node 2
After clicking node 2
Update 1
I created a live example at stackblitz which isn't working yet caused by other failures I make and can't find.
The components I use are:
Network
Node
Edge
Edge and Node are extending Module
I reworked my MyNetwork component according to some mistakes xadm mentioned.
Components (espacially dynamic) shouldn't be stored in state.
I implemented two new functions nodes() and edges() // line 15-41*
key prop should be used, too.
key is used now // line 18 + 32*
Passed props cannot be modified, you still have to copy initial data
into state. State is required for updates/rerendering.
line 9*
*line numbers in live example I mentioned above
Update 2
I reworked my code and now the life sample is working.
My hope is that I could use the native vis.js events and use them in MyNetwork or other Components I will write.
I read about using 3rd Party DOM event in this question can't figure out to adapt it for my particular case. Because I don't know how to attach the event handler to . Is this possible to do so I can use the event in other components?
Or should I open another question for this topic?
I see several possibilities of problems here.
<Decorator/> should be defined outside of <MyNetwork /> class. Click handler should be passed as prop.
Components (espacially dynamic) shouldn't be stored in state. Just render them in render or by rendering method (called from render). Use <Node/> components with decorator prop, key prop should be used, too.
Passed props cannot be modified, you still have to copy initial data into state. State is required for updates/rerendering. You probably need to remove edge(-es) while removing node.
Create a working example (on stackblitz?) if a problem won't be resolved.
It sounds like React is re-initializing your component when you are clicking a button. Maybe someone smarter than I am can figure out why that is happening...
But since no one has commented on this yet, one way I have handled these sorts of issues is to take the state management out of the display component. You say you are passing the nodes and edges via props from a parent component. You might consider moving the addNode, removeNode, createEdge, and other methods up to the parent component so that it is maintaining the state of the node/edge structure and your display component <MyNetwork/> is only displaying what it receives as props.
Perhaps this isn't an option in your app, but I generally use Redux to remove the state management from the components all together. I find it reduces situations like this where "who should own the state" isn't always clear.

Why use props in react if you could always use state data?

I understand that there's two ways to pass components data: props and state. But why would one need a prop over a state? It seems like the state object could just be used inside the component, so why pass the prop parameters in markup?
Props are set externally by a parent component. E.g.;
render() {
return <ChildComponent someProp={someValue}/>;
}
State is set internally, and often triggered by an user event within a child. E.g.;
handleUserClickedButton: () {
this.setState({
buttonClicked: true
});
},
render() {
return <button onClick={this.handleUserClickedButton}/>;
}
So, props are a way for data to go from parent to child. State is a way for data to be managed within a singular component, and possibly have changes to that data triggered by children. In effect, they represent data traveling in 2 opposite directions, and the way in which they are passed is entirely unique.
There are two ways to "pass" or access data from outside your component but state is not one of them.
The two ways are:
Props - which a parent component pass down to the child component.
Context - which you can "skip" the direct parent in the tree.
The state is an internal object which no other component has access to it unless you pass it explicitly (via the two ways mentioned above).
So basically your question is not accurate as you can't really compare the two.
I think what you are really asking is why using a state-less instead of a state-full component.
Which you can find an answer here in Stack-overflow or in other websites.
Edit
A followup to some of your comments.
why does the child not just have a shared state? for example, each
component (or sub-component) could just do a "this.state" to get the
current state of the program
The same way you can't share or access private objects in other
functions.
This is by design, you share things explicitly and you will pass
only what the component needs. For example, look it this page of
stack-overflow, lets say the voting buttons are components, why
would i pass them the whole state if it only needs the vote count
and 2 onClick event listeners? Should i pass the current logged in
user or maybe the entire answers rendered in this page?
so you can't pass state between a parent to child? for example, can't
the parent change the state and then the child gets the new state
This is exactly what the props or context should do, provide an API for sharing data between parents and children though we keep it in a one way data flow, from parents to children, you can't pass props upwards. but you invoke handlers passed down to your child components and pass data through that handler.

react.js does not update DOM after having changed state array

I am trying to implement a game in react where I have the board as an two dimensional array in the initial state of the parent class. Tiles are rendered by iterating through that array. I pass those children a function as a prop so that they can change that state array.
Now, when I use that function to change the array, the HTML does not update. The array gets updated when I call setState but it never rerenders. I tried this.forceUpdate() but still no luck. What I then did was to pass a function from that child to the parent through the function to update that child's state and this works, but I need the function from the parent to call itself recursively to update the board. I feel like I might have hit an anti-pattern. How could I change my code in order for the DOM to update, please?
There is parts missing but those are all the components involved. statusBoard is the internal version of the board featuring the solution. I hope this is clear.
Whenever you are linking props and state together you have to create a componentWillReceiveProps function like so:
componentWillReceiveProps: function(nextProps) {
this.setState({
positionX: nextProps.columnPosition,
positionY: nextProps.rowPosition
});
}
When games state changes and passes it down as props to Field, fields own internal state doesn't get updated at that point because it is relying on its own state. this is an anti pattern and you should avoid Field having to have any state and just rely on its props it gets passed and have a parent component that is handling the state of everything.
https://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html

Structuring my React/Redux app for efficiency

Im building an audio workstation app that will display a table of tracks containing clips. Right now I have a table reducer which returns a table object. The table object contains track objects and the track objects contain clip objects. I have a TableContainer which subscribes to the table store. My issue is I believe my app will be inefficient because it will re render the page every time a clip is added or manipulated. In reality only the particular track in which the clip resides would need to be re rendered right? How can I structure my app so not every little change re renders the entire app?
In the mapStateToProps of any component, don't select parent objects as a whole to send to the component. If possible select specific properties all the way to the leaf values. If your TableContainer's render() itself doesn't use the tracks array them make sure only the sibling properties that you do use get passed.
So instead of:
function mapStateToProps(state, props) {
return {
table: state.tables[props.tableId];
}
}
Do:
function mapStateToProps(state, props) {
let table = state.tables[props.tableId];
return {
name: table.name,
type: table.type
};
This allows React Redux to be more discerning when it comes to determining whether your component needs to be rerendered. It will see that even though the table had changed due to a clip change, neither the name, nor the type has changed.
However, since your Tablecomponent likely renders the Track components as well, you're likely not going to be able to avoid render calls. If any property anywhere up the tree gets altered, the tracks array also gets altered.
The solution in this case is to have the tracksarray not contain the entire track object but instead only a list of track IDs. You can then store the tracks alongside the tables and a change in one won't affect the other. Note that this only works if you do not go and fetch and pass the track object in the mapStateToProps of the Table component. You should make theTrack component in such a way that it accepts its ID instead of the actual object as a prop. This way the Table component is not dependent on the contents of the tracks at all.
The power of react is to re-render only what needs to be (by using the virtual DOM to make the comparison and the shouldComponentUpdate function).
I wouldn't look too much into it before it becomes a performance problem.
If it does, I would store the tracks in a separate directory and don't pass it to the app (main) component. In your Clip component's mapStateToProps function (if you use react-redux), fetch the track from the state as you get it's name from the props. This way if the track changes a lot (because of async fetching of slices for example), only the component will update.

setState not triggering a re-render when data has been modified

Just started working with React, and it's been going relatively smooth up until this point.
I have a list, that contains X pre-defined items, which means that the list is always rendered with a given amount of rows.
This data is collected from a REST API, prior to the list rendering. The data contains variables relating to the list, as well as an array that contains each item within the list.
I chose the easy route, so I populate every component with a single JSON object named 'data', that contains everything necessary.
Rendering the list looks something like this:
<MyList data={data} />
Then, in the getInitialState of MyList:
dataInt: this.props.data.dataInt || 0,
dataStr: this.props.data.dataStr || '',
rows: this.props.data.rows || []
So I store my array of items (JSON) in the initial state of the list (parent), and when I choose to render the list, I first create all components (children) in an array, like this:
var toRender = [];
for(i = 0; i < this.state.rows.length; i++) {
toRender.push(<ItemRow data={this.state.rows[i]} />);
}
and then I render like this:
return (
<div className="item-container">
<table className="item-table">
{toRender}
</table>
</div>
);
The render-function of MyItem look something like this:
return (
<tr>
<td>{this.state.dataFromItem}</td>
</tr>
);
Now, let's say I want to modify the child data from within the parent, with some new data I just got from the API. Same structure, just the value of one field/column has changed:
i = indexOfItemToBeUpdated;
var newRows = this.state.rows;
newRows[i] = data; // New JSON object with same keys, different values.
this.setState({ rows: newRows }); // Doesn't trigger re-render
this.forceUpdate(); // Doesn't trigger re-render
What am I doing wrong?
At first I thought it was because I was wrapping the render function of MyItem in a , but since it renders perfectly fine on the initial render, I will assume that is an acceptable wrapper.
After some testing, it seems that the parent view is re-rendered, but not the children (which are based on the data that is updated within the parent).
JSfiddle: https://jsfiddle.net/zfenub6f/1/
I think the problem could be the your are only using getInitialState for the Row. You are setting state with the passed in props. If the children get new props, getInitialState won't get called again. I always refer back to https://facebook.github.io/react/docs/component-specs.html to see all the lifecyle events. Try using componentWillReceiveProps(object nextProps) to set the state again. If you don't actually need state in the child, remove the use of state and just use props and it probably will work.
If you're children are using State and state is not updating, what about props? does that render just fine? If the parent changes the state of the children and the children don't reflect that change those child components most likely need a unique key assigned to them which tells React that these items need to be updated when the state is updated on the parent. I ran into a similar issue where props inside the child updated to reflect the parent but the state of the child would not update. Once I added keys, the solution was resolved. For further reading on this check out this blog that initially hinted the problem to me.
http://blog.arkency.com/2014/10/react-dot-js-and-dynamic-children-why-the-keys-are-important/
There is also the official documentation from React that explains this situation.
http://facebook.github.io/react/docs/multiple-components.html#dynamic-children

Categories

Resources