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
Related
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.
I have an array of usernames ["frank", "john", "stevie"]. looping through each element i create a ChatBox component:
{
popups.map((x, i)=>(
<ChatBox
addMessage={this.addMessage}
delMessage={this.delMessage}
setMessages={this.setMessages}
getMessages={this.getMessages}
name={x}
users={this.state.users}
id={x}
key={i}
user={this.fetchUser(x)}
/>
))
}
It looks pretty straight forward. I need to make an ajax call once the components are rendered, so I call this:
componentDidMount() {
console.log(this.props.name)
//make ajax call using `this.props.name`
}
The problem: I noticed that the rendered components set the name props with the initial value of "frank" in ["frank", "john", "stevie"]. i.e if I have five elements in the array, the last four components have an initial value of the first element in the array!. And its very important an ajax call is made once the component is rendered, but if the calls are made using the value of the first element in the array, the resulting components will all have the same data as the first component. that's flawed.
I tried getDerivedStateFromProps() and componentDidUpdate() but this looks hacky as the first component that is rendered has the correct value and rarely changes so the ajax call isn't made.
Is there a way I can have all rendered components to have the correct props so i can make the ajax call on componentDidMount.
#FrankDupree this might be happening as you are passing array index as the key value to the chatbox component. Try passing the itemName as key value for the ChatBox component.
<ChatBox
addMessage={this.addMessage}
delMessage={this.delMessage}
setMessages={this.setMessages}
getMessages={this.getMessages}
name={x}
users={this.state.users}
id={x}
key={x}
user={this.fetchUser(x)}
/>
I think this might work for you.
Please go through: https://medium.com/#vraa/why-using-an-index-as-key-in-react-is-probably-a-bad-idea-7543de68b17c for a better understanding.
I need to rerender child component when the array passed by prop has any of its element changed from xhr request. The child component is passed an empty array (allFiles) at the beginning and inside the child component I used v-for to display a row for each array element. At any time user can add a new element to the array and that shows few information on each row. As soon as the element is added I make an xhr request to get additional information and when the information is returned I show the new information on the rows.
To achieve this on the child component that holds the rows of the array elements I passed :key="JSON.stringify(allFiles)"
The element looks like this:
<file-edit-list :all-files="allFiles" :key="JSON.stringify(allFiles)"></file-edit-list>
This works. However I have put a console.log(this.allFiles) at the created hook of the child element. I see a huge amount of rerender happening for the child component. For each line of the following for in loop where I map the response to the old array, I see a rerender.
for (const k in response) {
if (response[k]) {
this.allFiles[fileIndex][k] = response[k];
}
}
I don't think this is optimal. How can I make the performance better while keep all my functionality working as is.
I'm pretty new to react and have been working on this new page for work. Basically, there's a panel with filter options which lets you filter objects by color. Everything works but I'm noticing the entire filter panel flickers when you select a filter.
Here are the areas functions in the filter component I think bear directly on the filter and then the parent component they're inserted into. When I had originally written this, the filter component was also calling re render but I've since refactored so that the parent handles all of that - it was causing other problems with the page's pagination functionality. naturally. and I think that's kind of my problem. the whole thing is getting passed in then rerendered. but I have no idea how to fix it. or what's best.
checks whether previous props are different from props coming in from parent and if so, creates copy of new state, calls a render method with those options. at this point, still in child component.
componentDidUpdate(prevProps, prevState) {
if (prevState.selectedColorKeys.length !== this.state.selectedColorKeys.length ||
prevState.hasMatchingInvitation !== this.state.hasMatchingInvitation) {
const options = Object.assign({}, {
hasMatchingInvitation: this.state.hasMatchingInvitation,
selectedColorKeys: this.state.selectedColorKeys
});
this.props.onFilterChange(options);
}
}
handles active classes and blocks user from selecting same filter twice
isColorSelected(color) {
return this.state.selectedColorKeys.indexOf(color) > -1;
}
calls to remove filter with color name so users can deselect with same filter button or if its a new filter, sets state by adding the color to the array of selected color keys
filterByColor(color) {
if (this.isColorSelected(color.color_name)) {
this.removeFilter(color.color_name);
return;
}
this.setState({
selectedColorKeys:
this.state.selectedColorKeys.concat([color.color_name])
});
}
creating the color panel itself
// color panel
colorOptions.map(color => (
colorPicker.push(
(<li className={['color-filter', this.isColorSelected(color.color_name) ? 'active' : null].join(' ')} key={color.key} ><span className={color.key} onClick={() => { this.filterByColor(color); }} /></li>)
)
));
parent component
callback referencing the filter child with the onFilterChange function
<ThemesFilter onFilterChange={this.onFilterChange} />
onFilterChange(filters) {
const { filterThemes, loadThemes, unloadThemes } = this.props;
unloadThemes();
this.setState({
filterOptions: filters,
offset: 0
}, () => {
filterThemes(this.state.filterOptions.selectedColorKeys, this.state.filterOptions.hasMatchingInvitation);
loadThemes(this.state.offset);
});
}
when I place break points, the general flow seems to be :
filterByColor is triggered in event handler passing in that color
active classes are added to the color, a filter tag for that color is generated and appended
componentDidMount takes in the previous props/state and compares it to the new props/state. if they don't match, i.e state has changed, it creates a copy of that object, assigning the new states of what's changed. passes that as props to onFilterChange, a function in the parent, with those options.
onFilterChange takes those options, calls the action method for getting new themes (the filtering actually happens in the backend, all I really ever need to do is update the page) and passes those forward. its also setting offset to 0, that's for the page's pagination functionality.
It looks like the problem might be around the componentDidUpdate function which, after setting breakpoints and watching it go through the steps from filterByColor to componentDidMount, that componentDidMount loops through twice, checking again if the colorIsSelected, and throughout all that the color panel pauses to re-render and you get a flicker.
Is it possible creating the copy is causing it? since it's being treated, essentially, as a new object that isColorSelected feels necessary to double check? any help you guys have would be much appreciated, this shit is so far over my head I can't see the sun.
Can you change
componentDidUpdate(prevProps, prevState)
with
componentWillUpdate(nextProps, nextState)
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.