Alter react component state properly - javascript

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.

Related

How to force an update of a subcomponent?

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.

Attempting to lift React state while maintaining access to a variable calculated lower down

I am learning React, and trying to understand exactly how much state should be lifted up to a higher level. It is my understanding that, generally speaking, a piece of state should only be lifted up if it is needed by multiple components bearing a common ancestor.
In my case, I am attempting to build a Calendar which reacts to being clicked on. The calendar itself should physically react to being clicked, but also a sidebar should show additional information on the day that was selected. Currently, the overall structure resembles this:
App
Calendar
Header
Square
Sidebar
LoginInterface
My desired behavior leads me to believe that the App should have ownership of the handleClick(squareNum) method, because it could pass it down as a prop to the Squares, but the handleClick method would be able to influence the information rendered on the Sidebar. My problem arises when I attempt to raise my state up without taking essentially the entirety of the Calendar implementation with me.
Obviously, lifting up just the handleClick(squareNum) is problematic, because squareNum is not defined at that level. When I rendered my Squares in the Calendar, I passed them the handleClick function as a prop, along with the squareNum (literally the number of the Square in its creation during the loop). In order for handleClick to be lifted up into App, I would also need to also lift up the creation of the Squares themselves. I think that I could do that, then pass <div>{row}</div> down to Calendar as {props.children}, but I feel like I'm really torturing the logic of the program at that point, and not adhering to the mentality of only lifting up the code that is needed by other components.
The React docs mention something similar to my problem in https://reactjs.org/docs/handling-events.html#passing-arguments-to-event-handlers, but I don't think that meshes perfectly here. While I do want to pass arguments to an event handler, I want to do that at a middle-level component, I want to pass the id of the Square in at Calendar but handleClick itself I want in App.
This doesn't seem like an uncommon use-case, so I feel like I must be missing something fundamental to React. Can anyone advise me on how to implement a mid-level argument into a passed-down function prop, or something along those lines?
class Calendar extends React.Component{
constructor(props){
super(props)
this.setState({
squares: Array(42).fill(null),
})
}
handeClick(i){
let squares = this.state.squares.slice()
squares[i] = 'clicked!'
this.setState({squares: squares})
}
renderSquare(squareNum){
return(
<Square
//other props
onClick={props.handleClick(squareNum)}
/>
}
render(){
let squareNum = 0;
let cal = [];
return(
for(let i=0;i<5;i++){
let row = [];
for (let j=0;j<7;j++){
row.push(this.renderSquare(squareNum));
squareNum++;
}
cal.push(<div //key //className>{row}</div>);
}
);
}
}
function Square(props){
//irrelevant for this example
}
class App extends React.Component{
constructor(props){
super(props)
this.setState=({
renderedDate: //combined with the squareNum, this allows calculation of which day was clicked
})
/**HandleClick would ideally be here so that the sidebar can have the selected date passed down to it as a prop**/
render(){
<div>
<Calendar //renderedDate />
<Sidebar //props />
</div>
}
}
For what you're trying to do - share information between one component and another somewhere else in the hierarchy - lifting the state up to a component that's an ancestor of both is the right thing to do. But that doesn't mean that the handler functions also need to be defined at that top level - that'd quickly become messy and hard-to-manage with any reasonably sized app.
All you need to have (and pass down) at the top level is the state and state setter. Pass the state down to the sidebar, and the state and the state setter down through the Calendar to the squares. Then, inside individual squares, define the click handler there, and call the state setter when needed.
// App:
render(){
<div>
<Calendar renderedDate={this.state.renderedDate} />
<Sidebar setRenderedDate={d => this.setState({ renderedDate: d }) />
</div>
}
Then pass down setRenderedDate until getting to Square, and have Square call that function (a prop) in its click handler.

Conditional Rendering of Arrays in React: For vs Map

I'm new to React and building a calendar application. While playing around with state to try understand it better, I noticed that my 'remove booking' function required a state update for it to work, while my 'add booking' function worked perfectly without state.
Remove bookings: requires state to work
const [timeslots, setTimeslots] = useState(slots);
const removeBookings = (bookingid) => {
let newSlots = [...timeslots];
delete newSlots[bookingid].bookedWith;
setTimeslots(newSlots);
}
Add bookings: does not require state to work
const addBookings = (slotid, tutorName) => {
timeslots[slotid].bookedWith = tutorName;
}
I think that this is because of how my timeslot components are rendered. Each slot is rendered from an item of an array through .map(), as most tutorials online suggest is the best way to render components from an array.
timeslots.map(slot => {
if (!slot.bookedWith) {
return <EmptyTimeslot [...props / logic] />
} else {
return <BookedTimeslot [...props / logic]/>
}
})
So, with each EmptyTimeslot, the data for a BookedTimeslot is available as well. That's why state is not required for my add bookings function (emptyTimeslot -> bookedTimeslot). However, removing a booking (bookedTimeslot -> emptyTimeslot) requires a rerender of the slots, since the code cannot 'flow upwards'.
There are a lot of slots that have to be rendered each time. My question is therefore, instead of mapping each slot (with both and information present in each slot), would it be more efficient to use a for loop to only render the relevant slot, rather than the information for both slots? This I assume would require state to be used for both the add booking and remove booking function. Like this:
for (let i=0;i<timeslots.length;i++) {
if (!timeslot[i].bookedWith) {
return <EmptyTimeslot />
} else {
return <BookedTimeslot />
}
}
Hope that makes sense. Thank you for any help.
Your addBooking function is bad. Even if it seems to "work", you should not be mutating your state values. You should be using a state setter function to update them, which is what you are doing in removeBookings.
My question is therefore, instead of mapping each slot (with both and information present in each slot), would it be more efficient to use a for loop to only render the relevant slot, rather than the information for both slots?
Your map approach is not rendering both. For each slot, it uses an if statement to return one component or the other depending on whether the slot is booked. I'm not sure how the for loop you're proposing would even work here. It would just return before the first iteration completed.
This I assume would require state to be used for both the add booking and remove booking function.
You should be using setTimeslots for all timeslot state updates and you should not be mutating your state values. That is true no matter how you render them.

React classes in main component constructor

Let's say I have a lot of app state to manage in my React application.
Therefore, I would like to split the state into smaller, manageable chunks.
For example I have the following main component with state and methods that alter this state.
class App extends Component {
constructor(props) {
super(props);
this.state = {
foo: ['some', 'items'],
bar: [{ arr: 'of objects'}]
}
}
changeFoo() {some code in here...}
changeBar() {some code in here...}
}
The state and methods written in the App component are getting out of hand. Yet it must be written in the App component since the state is passed to other components as props.
How would you usually manage this?
When you see that the state of your React application is getting out of hand, it's usually time to bring in a state management library like Redux (there're a few and Redux is the most popular one).
It'll help you have a global state that is managed in a reasonable way.
When we see how React works. It is based on one-directional data flow.
So, usually the Application state is kept at the top most Component (Say, App Component) in your case. So that data/state can be passed down as props to the component that needs it.
There, however may be the cases where children components of the parent, needs to work with the same data(Say in case of an event - a button click that happens in the child component.) In that case we write a function in the parent component and pass the function as props to the children, so that the state gets updated in the parent itself and any child gets updated data.
In pure React (without using any state management library), we have to pass the state as props to work with our app. But in case you choose to use a state management library such as Redux, then the components (known as Containers) can directly communicate with the Application State.
And If your application state contains objects within objects(like you have shown) or Array of Objects containing more Objects, then you cannot use setState() to update the state directly. In most of the cases, you take copy of the state and then use JSON.parse(JSON.stringify(state)) to do deep cloning and work with the state in a best possible manner.
There are other things in the example, the functions that you have used within the class , you need to bind the scope of this variable to point to the current class. This we do inside the constructor method, or simple make use of arrow function in order to avoid errors.
If you need more explanation, I will share with you :)
One solution is to make a generic change() function with a parameter for the key that should be changed:
change(key, value) {
setState({key: value, ...this.state});
}
Now when you want to add a listener to a child component:
<Foo onChange={ value => change('foo', value) }/>
<Bar onChange={ value => change('bar', value) }/>

React Component Flickers on Rerender

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)

Categories

Resources