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)
Related
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.
I am new in React.js, and I have an issue with parent component state updating. I attached whole code so that don't miss something that can be important.
I need my 'Reset' button to reset parent component's state
HERE you can see what the issue is.
You should open 'Events' in Navigator menu and try to select categories and then to reset them.
parent component: components/events/filter-bar.js
chils component: components/shared/dropdown/category.js
So, you have categories in FilterBar and Category components state - which are not in sync. Since FilterBar also recieves categories as a prop - that indicates that you don't have a single source of truth for categories. There are at least three places where you are keeping categories in different components states. That is very bad thing and leads to bugs and hard to maintain state. Solution is having categories state in just one place and than passing it down to components which need them (together with methods for updating categories).
Anyway, if reseting only dropdown categories is what you want this is what you can do:
create a new method in FilterBar, name it whatewer you want and pass it as a prop to Categories component. This method will accept just one argument - array of categories. This method will update FilterBar state.categories.
in Categories component, remove state.selected and saveSelected method. In places where you are using saveSelected replace it with prop (function/method) passed from FilterBar.
update
This question actually has nothing to do with component lifecycles or updating parent state.
Here is a thing: you can't control the state you don't own (which is the case with 3rd party components). Some lib authors provide methods for setting initial state or reset, but that is not the case with component library you are using.
In your case, best you can do is to unmount dropdowns ad then mounts them again. It's something like refreshing just the part of the page.
Do this:
add visible: true to the FilterBar state
add onReset method to the same component:
onReset = () => {
this.setState(
state => ({ visible: false }),
() => {
this.setState({ visible: true });
}
);
};
update reset button: onClick={this.onReset}
conditionally render ButtonToolbar:
return this.state.visible ? <ButtonToolbar /> : null // have shortened code just for demonstration purpose
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.
Asking for best practice or suggestion how to do it better:
I have 1 global reusable component <MainMenu> inside that component I'm doing XHR request to get menu items.
So if I place <MainMenu> in header and footer XHR will be sent 2 times.
I can also go with props to get menu items in main parent component and pass menu items to <MainMenu> like:
<MainMenu :items="items">
Bet that means I cant quickly reuse it in another project, I will need pass props to it.
And another way is to use state, thats basically same as props.
What will be best option for such use case?
If you don't want to instantiate a new component, but have your main menu in many places you can use ref="menu" which will allow you to access it's innerHTML or outerHTML. I've created an example here to which you can refer.
<div id="app">
<main-menu ref="menu" />
<div v-html="menuHTML"></div>
</div>
refs aren't reactive so if you used v-html="$refs.menu.$el.outerHTML" it wouldn't work since refs are still undefined when the component is created. In order to display it properly you would have to create a property that keeps main menu's HTML and set it in mounted hook:
data() {
return {
menuHTML: ''
}
},
mounted() {
this.menuHTML = this.$refs.menu.$el.outerHTML;
}
This lets you display the menu multiple times without creating new components but it still doesn't change the fact that it's not reactive.
In the example, menu elements are kept in items array. If the objects in items array were to be changed, those changes would be reflected in the main component, but it's clones would remain unchanged. In the example I add class "red" to items after two seconds pass.
To make it work so that changes are reflected in cloned elements you need to add a watcher that observes the changes in items array and updates menuHTML when any change is noticed:
mounted() {
this.menuHTML = this.$refs.menu.$el.outerHTML;
this.$watch(
() => {
return this.$refs.menu.items
},
(val) => {
this.menuHTML = this.$refs.menu.$el.outerHTML;
}, {
deep: true
}
)
}
You can also watch for changes in any data property with:
this.$refs.menu._data
With this you don't need to pass props to your main menu component nor implement any changes to it, but this solution still requires some additional logic to be implemented in it's parent component.
So let's say there is acomponent which displays 2 child components: a document list and the selected document. By default the selected document component is not rendered, only when a document is selected from the list. And i also want this whole thing work when a new document is selected from the list.
There is a state which holds the document content and responsible for the selected document rendering, so i thought i'm going to set it to null in the method which handles the list item selection in order to unmount the previously created child component. Like this (excerpts from the parent class):
handleResultListItemClick(docname) {
if (this.state.sectioncontainer != null) this.setState({sectioncontainer: null},()=>{console.log("muhoo");});
var selected_doc = this.state.resultlist.filter((doc) => {
return docname === doc.properties.title;
});
this.setState({sectioncontainer: selected_doc[0].content.sections},()=>{console.log("boohoo");});
}
...
render() {
return (
...
{this.state.sectioncontainer != null && <SectionContainer listOfSections={this.state.sectioncontainer}/>}
);
}
The only problem is that state handling is not fast enough (or something) in react, because putting the state nullification and its new value setting in the same method results in no change in ReactDOM.
With the above code, the component will be created when the parent component first rendered, but after selecting a new doc in the list results in no change.
How should i implement this in way which works and also elegant?
I found this: ReactDOM.unmountComponentAtNode(container) in the official react docs. Is this the only way? If yes, how could i get this container 'name'?
Edit:
Based on the answers and thinking the problem a bit more through, i have to explain more of the context.
As kingdaro explained, i understand why there is no need to unmount a child component on a basic level, but maybe my problem is bit more sophisticated. So why did i want to unmount the child?
The documents consist of several subsections, hence the document object which is passed to the child component is an array of objects. And the document is generated dynamically based on this array the following way (excerpt from the SectionContainer class which is responsible to display the document):
buildSectionContainer() {
return this.props.listOfSections.map((section, index) =>
{
if (section.type === 'editor') return (
<QuillEditor
key={index}
id={section.id}
modules={modules}
defaultValue={section.content}
placeholder={section.placeholder}
/>
);
else if (section.type === 'text') return (
<div key={index}>{section.value}</div>
);
}
);
}
render() {
return (
<div>
{this.buildSectionContainer()}
</div>
);
}
The SectionContainer gets the array of objects and generate the document from it according to the type of these sections. The problem is that these sections are not updated when a different doc is selected in the parent component. I see change only when a bigger length array is passed to the child component. Like the firstly selected doc had an array of 2 elements, and then the newly selected doc had 3 elements array of sections and this third section is added to the previously existing 2, but the first 2 sections remained as they were.
And that’s why i though it’s better to unmount the child component and create a new one.
Surely it can happen that i miss something fundamental here again. Maybe related to how react handles lists. I just dont know what.
Edit2:
Ok, figured out that there is a problem with how i use the QuillEditor component. I just dont know what. :) The document updates, only the content of QuillEditors doesnt.
The reason your current solution doesn't actually do anything is because React's state updates are batched, such that, when setState is called a bunch of times in one go, React "combines" the result of all of them. It's not as much of a problem with being "not fast enough" as it is React performing only the work that is necessary.
// this...
this.setState({ message: 'hello', secret: 123 })
this.setState({ message: 'world' })
// ...becomes this
this.setState({ message: 'world', secret: 123 })
This behavior doesn't really have much to do with the problem at hand, though. As long as your UI is a direct translation of state -> view, the UI should simply update in accordance to the state.
class Example extends React.Component {
state = {
documentList: [], // assuming this comes from the server
document: null,
}
// consider making this function accept a document object instead,
// then you could leave out the .find(...) call
handleDocumentSelection = documentName => {
const document = this.state.documentList.find(doc => doc.name === documentName)
this.setState({ document })
}
render() {
const { document } = this.state
return (
<div>
<DocumentList
documents={this.state.documentList}
onDocumentSelection={this.handleDocumentSelection}
/>
{/*
consider having this component accept the entire document
to make it a little cleaner
*/}
{document && <DocumentViewer document={document.content.sections} />}
</div>
)
}
}