Interacting with the parent component in react - javascript

I've been thinking about a certain abstraction that I want to create in my codebase, which would eliminate a lot of boilerplate. The question I have is about the most "correct" way to achieve an interaction between child and parent classes.
We have a lot of <table />s which I want to abstract into a sort of <Table /> component. The reason for this is that we have a lot of boilerplate to do with sortable headers, pagination, etc.. Every time we implement a new table, there's a lot of copy-pasting and a lot of extra testing which should really be abstracted away.
My ideal pattern would be something like:
<Table>
<Table.Header>
<Table.SortableHeader dataKey="id" default>ID</Table.SortableHeader>
<Table.SortableHeader dataKey="name">Name</Table.SortableHeader>
// ...
</Table.Header>
<Table.PaginatedBody pageSize=15 rowElement={ MyRowElement } />
</Table>
In order to do this, the <Table.SortableHeader> components need to be able to set the state (or otherwise interact) with the parent <Table> component in order to change its sort key/order, and the sort key/order needs to be passed down to the <Table.PaginatedBody> component.
Furthermore, the <Table.SortableHeader> components needs to know the current sort key/order as they will display different depending on whether the sort key is the same as their dataKey and also if the sort order is asc or desc.
One way I thought of doing this was by passing the parent component down in the context (I know new context-based stuff has come out but this is a more general question of principle).
Does this method throw up any obvious problems, and are there any other standard methods for doing this sort of thing? I want to avoid having configuration objects which I pass down to a <GenericTable /> component which will then generate the structure, as I feel like JSX is a perfectly good DSL for creating view elements, and this is a lot cleaner.

You can define your sort handler in your Table component and add it to it's children props along with the sort state. This way Header components can call the parent sort handler to update the Table state, which will then pass down the sort state to the Header props.
You can use cloneElement to add props to children :
{ React.Children.map(this.props.children, (child) => {
return React.cloneElement(child, {
sort: this.state.sort,
handleSort: this.handleSort
});
})}
Full sample code : https://codesandbox.io/s/926nj5r6jw

Related

React.js, trigger a listener function on particular actions in multiple components

I am developing an app, that has three room containers/components: available, applied, joined.
After clicking: 'Apply' for room, or 'Remove' I wanted to post some data to my server and, on the front-end side, wanted to update my UI.
So in order to avoid unnecessary requests to my server (refetching data), what I wanted to achieve is to trigger a listener function in the other component, that would pass some data (room) to it and will add/remove room from the list.
Now, the serious question is - how could I achieve it? Does even React provide some built-in listener pattern? Or is there any other possible solution to this case?
If not, would a good solution be, to pass a function in props add/remove (room) to all three components from the parent component, and then pass as an argument room data and to which component should it be passed in?
I have a very little experience in React and it is hard for me to evaluate, which way would be the best.
First of all, your plan to centralize the logic in a parent component (instead of each child component calling its own methods) is 100% correct. This way state management can be consolidated into one common place.
One approach is to define an addRoom() and a removeRoom() method in your parent component:
addRoom(roomName) {
//update your local state...
//make external requests...
}
removeRoom(roomName) {
//update your local state...
//make external requests...
}
To each child component, you pass props for addRoom(), removeRoom(), and the current state:
<ChildOne
addRoom={this.addRoom}
removeRoom={this.removeRoom}
roomState={yourStateObject}
/>
<ChildTwo
addRoom={this.addRoom}
removeRoom={this.removeRoom}
roomState={yourStateObject}
/>
<ChildThree
addRoom={this.addRoom}
removeRoom={this.removeRoom}
roomState={yourStateObject}
/>
Inside each of your child components, you have access to the common/shared state, and you have the ability to call addRoom() and removeRoom(), which will cause the parent component to update that common/shared state, which in turn trickles down to all the child components.
Each child component can choose to make use of whatever parts of the shared state that it needs to.
When a child component needs to make a call to addRoom() or removeRoom(), it passes the name or id of the room as an argument:
<div onClick={this.props.addRoom.bind(null, 'A')}>Add Room A</div>
<div onClick={this.props.addRoom.bind(null, 'B')}>Add Room B</div>

How to filter children components in Vue based on their component type?

I am having trouble identifying children components in Vue.
I have this markup here:
<parent>
<child-one>
</child-one>
<child-two>
</child-two>
<child-one>
</child-one>
</parent>
The parent component is functional, so it is doing this:
render(createElement) {
return createElement('div', this.$slots.default);
},
I want to iterate over this.$children and return an Array of VNodes kind of like this:
const matchingChildren = this.$children
.filter(child => child.isChildOneType === true)
return createElement('div', {}, matchingChildren)
How do I do that?
What and where do I need to filter?
I'm pretty sure I can do it by setting a type prop on child-one, and then using this.$children.filter(child => child.$attrs.type === 'special'), but I want to filter them based on the fact they are child-one components.
I'm trying to make the example simple, but this is for tabbed pages, so I am not interested in conditional rendering. I'm trying to transform the markup into different DOM output, and I need to filter children of a specific type.
I have researched $attrs and $options as hooks that I could use, but if I use attrs, I need to add a prop to each component. I would like to avoid that since they are all of type child-one. I also can't seem to add a static property to child-one. The $options field is always empty.
There is nothing wrong or non-standard by setting a specific property to use as a filtering criteria such as
this.$children.filter(child => child.$attrs.type === 'special')
If your child components are siblings of other non-component markup, or those of you dont want filtered, then you have no choice but to set a prop indicating that they are the components that you need.
In the browser, the child nodes are plainly generic and cannot be properly isolated from one another without using some sort of identifier; such as a tag name, an id, attribute, or property. Nodes rendered via Vue, React, HTML, native JS whatsoever are all in all the same in the browser's perspective.
Since you can't use the component names like child-one because they will be rendered like normal HTML, you can just plainly add an attribute/v-attribute to each component wrapper on your Vue templates and simply filter them out like normal nodes once rendered.
<parent>
<child-one>
<div class="wrapper" componentType="1"></div>
</child-one>
<child-two>
<div class="wrapper" :componentType="data.type"></div>
</child-two>
<child-one>
<div class="wrapper" componentType="1"></div>
</child-one>
</parent>
Or you can add attributes on the component themselves, which I have never tried before so I can't help you with that

Share data between React components with no relation?

I'm working on a React component library that allows for client-side data filtering by passing an array of objects and an <input/> as props to a <SearchFilter/> component. I want to return the filtered results to a separate <SearchResults/> component that can be rendered elsewhere in the tree (i.e. the results component doesn't have to be a child of the input component).
I've got the filtering figured out, but I'm not sure the best route to take in React on getting the filtered data to the <SearchResults/> component.
This is what I'd like to end up with...
<SearchFilter
data={data}
input={<input type="text" value={value} onChange={this.handleChange}/>}
/>
Then, using Render Props to return the data and map over that to return JSX, there would be the results component. Something like this...
<SearchResults
render={data => (
data.map(el => (
<div>
<span>{data.someProperty}</span>
</div>
)
)}
/>
This is what I'd like to achieve because I want to allow for rendering the <SearchFilter/> component at one place in the tree, and allow the <SearchResults/> component to be rendered elsewhere, so that there's maximum flexibility in how the tree is composed and, therefore, how the view is rendered.
I've looked into the Context API, but it seems like that would require a handful more components to be a part of my library, which further complicates what I'm trying to achieve. If that's the only way around it, then that's fine, but I wanted to ask and see if anyone can think of another solution.
Thanks!
The bigger issue is that you will need to manage a state that is shared between components on a higher level, i.e., any component that will wrap these other two components, ultimately. With plain React, this state would be managed by the parent (or ancestor) component, passing down the relevant values as props. This opposed to the, usually bad, idea to have sibling components influence each other's state, since you well get into the "who's boss here"-problem.
The thing the Context API handles is not having to pass down props for things that typically don't change (or: typically shouldn't cause renders to trigger often).
A global state store, such as Redux, can help you modelling this, but in essence it's not much more than 'a' component managing state, and other components rendering according to that state. Events within the lower components trigger changes in the data, which will cause the state to change, which will cause the props of the children to change, which then will cause re-renders.
I'd advise you to try using this simple pattern:
class Search ... {
state = {data: [], text: ""}
render() {
return (
<div>
<SearchFilter
data={this.state.data}
onSearch={() => this.fetchNewData()}
onChange={(e) => this.setState({text: e.targetElement.value})}
text={this.state.text}
/>
<SearchResults data={this.state.data} />
</div>
);
}
fetchNewData() {
fetch('/url?text=' + this.state.text)
.then((newData) => { this.setState({data: newData}); })
}
}
Something along these lines. If you have trouble modelling stuff like this, you can use Redux to force you to do it in a similar way, and avoid managing local state intermixing with global state (which is typically something that is hard to manage).
If you do this right, components that have no state (i.e., aren't responsible for managing state and thus have no event handlers) can all become pure components, i.e. stateless components, i.e. functions that return JSX based on props:
const SearchResults = ({data}) => <div>{data.map( () => <etc /> )}</div>
You could create a data store class that holds your filter, pass it in as a property to both components, and have your SearchFilter component change a value in that.

React: Bubbling up click events on nested components

I'm creating a react file tree, and I have the tree setup as a React component. The tree can take a contents prop that is an array of either strings, or other <Tree /> components (this enables the nested file structure UI). These tree components can be nested indefinitely.
I need to register a click event on the children of the nested tree components, but I'm having trouble getting it to work beyond the first level of nesting. A simplified example of what I'm dealing with:
//In App - the top level component
const App = React.createClass({
_handleChildClick () {
console.log("this is where all child clicks should be handled");
},
render () {
return (
<Tree
handleChildClick={this._handleChildClick}
contents={[
<Tree />
]}
/>
);
}
});
//And in the tree component
<div onClick={this.props.handleChildClick}></div>
If you want to see more detail - here's the github repo.
I tried researching this question and saw people using {...this.props} but I'm not sure if that applies to my scenario - if it does, I couldn't get it to work.
Thanks for any help on this.
The reason why the click handling does not work beyond the first level is because your second level Tree component (the one inside the contents array) does not get the appropriate prop handleChildClick passed in. (BTW I think the convention is to call the prop onChildClick while the handler function is called handleChildClick - but I digress.)
Do I understand correctly that you actually want to inform each layer from the clicked component up to the top? For this to happen, you need to extend the props of the tree component that is inside the contents array - it needs to receive the click handler of its parent component. Of course, you cannot write this down statically, so it needs to be done dynamically:
Your Tree component, before actually rendering its children, should extend each of them with the component's click handler, which can be done using the function React.cloneElement (see API documentation and a more detailed discussion). Directly applying this to your component makes things a bit messy, because you are passing the component's children in a prop, so you need to figure out which prop to modify. A bit of a different layout would help you quite a lot here:
<Tree handleChildClick={this._handleChildClick}>
<Tree />
</Tree>
looks nicer IMHO and makes the structure much clearer. You can access the inner components via this.props.children, and cloneElement will be much simpler to use.
So, in your Tree component, you could have a render method like this:
render () {
const newChildren = this.props.children.map(child =>
React.cloneElement(child, {onChildClick: this._handleChildClick}));
return (
<div>{newChildren}</div>
);
}
Please note that this code will not work if you have a mixture of strings and Tree components, therefore my third and last suggestion would be to wrap those strings into a very thin component to allow for easier handling. Alternatively, you can of course do a type comparison inside the map.

React/Flux implementation technique is unclear for when a parent component needs to pull strings on child component

I have a situation which isn't too contrived, and I'm having trouble implementing it using the React best practices. In particular it produces this error:
Uncaught Error: Invariant Violation: setProps(...): You called setProps on a component with a parent. This is an anti-pattern since props will get reactively updated when rendered. Instead, change the owner's render method to pass the correct value as props to the component where it is created.
The situation is like this. The parent contains a child component. The parent has event handlers for UI and for the behavior to work, something inside the child component needs to render its HTML with a CSS change to the height style. Therein lies the wrinkle, usually the information flows upward or stays put, but here I need to change something in the child.
Parent component (Widget) renders this:
<div class="Widget">
<div class="WidgetGrabBar" onMouseDown={this.handleMouseDown}>
<WidgetDetails heightProp={this.props.detailsHeight} />
</div>
And elsewhere in Widget I've got
componentDidMount: function() {
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
},
componentDidUnmount: function() {
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
},
<...>
handleMouseDown: function(e) {
e.preventDefault();
this.props.actuallyDragging = true;
},
handleMouseUp: function(e) {
this.props.actuallyDragging = false;
},
handleMouseMove: function(e) {
e.preventDefault();
if (this.props.actuallyDragging) {
// update the prop! I need to send an urgent package of information to my child!! jQuery or findDOMElement() followed by DOM traversal is forbidden!!!
this.setProps({
detailsHeight: this.props.detailsHeight + e.deltaY
});
}
},
And I had WidgetDetails' render() render something like:
<div class="WidgetDetails" style={height: this.props.heightProp}>
{detail_items_move_along_nothing_to_see_here}
</div>
I figured that rolling out the jQuery to grab .WidgetDetails to fiddle with its style attr is the wrong thing, the non-React way to go about it. The real anti-pattern.
But now I'm being told that I can't change my props. Or I have to throw out everything including the bathwater in order to have new props. I'm not doing that; my props contain the contents of the detail items. Maybe it is expensive to make another entirely new copy of this.
I'm trying to let React participate in this rendering work to put the new height in. How am I supposed to even do this? Is this error basically enforcing that Props are supposed to be immutable now? The error is telling me that I have to involve this height even farther up on the component chain. I can conceivably do so with a callback from up above, but this feels very wrong. I need to pass information downward, not upward.
Maybe I'm supposed to use state. But changing state forces Widget, the parent component to render. That is not what I desire. Only one singular place in the DOM needs to re-render, that is the child component's div's style attr.
There are two approaches. Either
call handlers on the parent. Then Pass the new props to the child via props. If I recall correctly, that's the approach the react hello world tutorial takes.
Mutate state in the view via setState.
In your case, it seems that approach 2 really makes sense. You are basically trying to track view data.
Never, by the way, update state directly. Use setState. The whole point of reacts virtual dom is that it's optimized for spurious updates, so you will be fine. There is also the life cycle method componentShouldUpdate in case you want finer control.
For completeness I should add that there's a third way of using a global store. That's what react flux adds. But again, in your case that's probably over kill.

Categories