How to avoid duplicating logic in react data table - javascript

I'm looking for help with structuring an entity data table component in react.
I'm using react-virtualized and am trying to determine how to encapsulate the boilerplate functionality used for server-side sorting/filtering/paging/row deletion/etc.
I started out with a simple, self-contained component which only required an API path, and handled everything by itself with local state.
Needing to be able to update/delete/add/refresh records from outside the table component, I moved the table state/logic into a container component that manages the table, along with some buttons and modal windows.
However, now that all the handlers for sorting/filtering/paging are in this container, it seems they would all have to be duplicated in order to be reused for other tables with slightly different requirements, but the exact same sorting/filtering/paging.
How can I contain the boilerplate table logic within the table component, while also being able to update/delete/add/refresh records from outside the component?
To visualize this, here is a contrived example where all the logic and state is encapsulated in the TableWithFilter component:
<SomeEntityList>
<TableWithFilter apiPath={this.props.apiPath}>
<FilterBar />
<Table />
<TableWithFilter>
</SomeEntityList>
the filter method and state could live in TableWithFilter:
TableWithFilter._handleFilter() {
// get filter, inject filter into api path, get records, update state
}
Now, lets say we need to add a modal so that we can edit the values in a row:
<SomeEntityList>
<EditRowModal />
<TableWithFilter>
<FilterBar />
<Table />
<TableWithFilter>
</SomeEntityList>
The state and filter method then have to be moved up to the SomeEntityList component so that the modal can change the records in the table:
SomeEntityList._handleFilter() {
//get filter, inject filter into api path, get records, update state
}
SomeEntityList._editRow() {
// make update call, update state
}
Assuming I'll have dozens of various SomeEntityListA/SomeEntityListB/SomeEntityListC with various difference (some have edit record modals, some have buttons to add new records, etc) how would I avoid duplicating the filtering logic in dozens of places?

The way I see it you have two options.
If the logic for the reusable filters are functional, you could probably move them out of the components entirely and put them in a utils directory or something.
Use higher-order components.
Edit
Why don't you just pass the filter as a param?

Ok, after some time, I decided to circle back to this to help anyone else out.
I've since moved to incorporation redux in the project (for many reasons).
On this topic specifically, I now have a CreateSearchableTable function that acts a factory for creating namespaced SearchableTable reducers. This allows the reuse of all the fetching/infinite scrolling/paging/filtering/sorting logic. I'm currently working on finding the best way to remove duplicated action creators.

Related

How to get Vue app container div attribute

I have a page which is being generated by my app in java (SSR) depending on some data (e.g. a publisher for some entity). I would like to create some sort of a reusable Vue component that would call an API method and request some data about the entity that is currently opened. Also in some cases there could be more than one such component on one page.
The only thing I cannot really figure out being a most-of-the-time backend developer - is how to tell a component which entity I'm trying to get. The only solution that comes to my mind is to generate the parent <div class="my-vue-component"><div> with an additional attribute, e.g. <div class="my-vue-component" publisher-id="123"><div>.
But I cannot find if there is a way to access that attribute from inside the Vue instance. (Please note that I don't have a fixed id for this div as there can be many such components on the same page referring to different data).
Any kind of advice is appreciated.
As stated in the previous answer, you will need to use props. Although since you will pass down data to multiple components and the data can change, there should be a way to respond to those changes.
For that, you will have to bind the prop with a reactive variable in your page/parent component.
So your SSR code should look like
<my-vue-component :publisher-id="openId"></blog-post>
And inside your page/parent component will reside the openId, which you can change as needed, and your component will re-render if prop passed to it changes.
export default {
data(){
return {
openId:1
}
}
}
It seems like you are looking for components-props.
You can define a prop like
Vue.component('blog-post', {
// camelCase in JavaScript
props: ['postTitle'],
Your SSR code should then be generating:
<!-- kebab-case in HTML -->
<blog-post post-title="hello!"></blog-post>
Inside the component methods you can access the passed in value using this.postTitle

Can I push objects into Vue component data? Wanting to make a table after js filtering/manipulation

I want to make a table. Actually, I am making a website with many pages with many tables, so I wanted to make a table component. The table data has not yet been put into the table because I need to manipulate the data a lot in js.
When js is done with it, I intended to push every row object into the data property of my Vue Component (to then do a v-for in the html to fill the table).
but I cant find anyone pushing data into vue components. Are the examples right under my nose?
if I don't push into components themselves, that means I need to push into the parent vm? which means a new data property per table instance ..?
I am really struggling putting together the bigger picture connection when it comes to connecting Vue with the outputs from js... Looking for any input
Pushing data into a vue component is not a good practice. You would have a lot easier time if you use Vuex for what you're trying to do. Then you could build a closed-loop data system that updates state data in Vuex with mutations and returns the update to component with a getter. Doing all of this within component is possible if you initialize your data property correctly, though.
To actually answer your question, though, you would do it like this:
data () {
return {
myData: [],
someDataObject: null
}
}
...
methods: {
fillData () {
this.myData.push(this.someDataObject);
}
}
And in template:
...
<div v-for(item in myData, index) :key:item item:item>
<input type:'text' v-model="someDataObject">
<button #click="fillData();"></button>
<p>{{myData[0]}}</p>
</div>
...

Does passing data in props duplicate that data?

I'm building a dashboard in React, and currently I'm looking at building a small button component that will allow a user to download data from a chart as a CSV file. Something like this:
// Dashboard
render(){
const data=makeLargeDataset(); // make an array with 1m rows
return (
<DownloadCSV data={data} />
);
}
// Download Button
onClick(){
if (this.props.data){
convertAndDownload(this.props.data);
}
}
Does this duplicate the 1M rows when passing to the button's prop? If I had to pass a prop through several layers, will it keep duplicating? Is there a smarter way to pass props -- or work with large variables and arrays, in general -- so it isn't as wasteful or inefficient?
Nope, It does not.
Any props passed from a higher order component to a child component always references the same props. It's called Single Source of Truth. You can alter one data point in the HO Component and React as the name tells reacts to only the change and nothing else.
A simple way for holding such big data sets would be to use a store like Redux to keep the data safe but the smarter way would be to create an API or a micro service based on your needs and call that rather than loading a million rows on the browser.
You are not passing the data copy, you are passing the reference.
You could think about paginating your data in some way in order to handle lower sized arrays.

React / Apollo shared query state

I have a very simple app, with a list view and a detail view. The list view is wrapped with a graphql query that maps to a data prop containing an array of items. Selecting one item instantiates a detail view, where the id of the item is used to compose another graphql query for that one item.
const ListView = graphql(ALL_ITEMS, ...)(ListViewComponent);
<ListView data={ [....] }>
<Detail>
<Next /> <Prev /> {/* <-- need info from ListView.data */}
</Detail>
{ this.props.data.map(....) }
</ListView>
The trouble I'm having is adding previous/next navigation to that detail view. The detail component is unaware of what is in that data prop containing all the items in the ListView component. In a typical redux application, I would store the items as global application state, and injecting them into a component would be trivial, but Apollo doesn't seem to work that way. Its state tree is not very consumable (e.g. using connect()).
I see a few possible solutions for this, and none of them seem great:
Wrap the prev/next navigation component with a graphql query that looks for currentOffset + 1 and currentOffset -1. This is definitely the most "apollo" solution, but the problem there is that currentOffset is unknown to that component because it doesn't have the data prop. Further, I would have to ensure that any other variables (e.g. filters, sort) on the list view query got passed through, and those are in the obscure apollo state tree, as well.
Listen to the APOLLO_QUERY_RESULT action in my reducer and maintain a second copy of the query results with all the information my other components need.
Use context to share the data prop to children.
Explicitly pass the data prop to children.
Seems like a common scenario. What's the right approach?
Passing data down as a prop would probably be the easiest solution.
One other option you may want to consider is simply wrapping whatever component you want to have access to the same query data with another graphql HOC. If you utilize the same query you use for ListView's HOC (ALL_ITEMS), you can use the data prop in your child just like you do in ListView. The neat thing about this approach is -- as long as you use the default fetchPolicy -- your child component will not trigger a second query against your server; it will simply utilize what's already in the cache.

Is connect() in leaf-like components a sign of anti-pattern in react+redux?

Currently working on a react + redux project.
I'm also using normalizr to handle the data structure and reselect to gather the right data for the app components.
All seems to be working well.
I find myself in a situation where a leaf-like component needs data from the store, and thus I need to connect() the component to implement it.
As a simplified example, imagine the app is a book editing system with multiple users gathering feedback.
Book
Chapters
Chapter
Comments
Comments
Comments
At different levels of the app, users may contribute to the content and/or provide comments.
Consider I'm rendering a Chapter, it has content (and an author), and comments (each with their own content and author).
Currently I would connect() and reselect the chapter content based on the ID.
Because the database is normalised with normalizr, I'm really only getting the basic content fields of the chapter, and the user ID of the author.
To render the comments, I would use a connected component that can reselect the comments linked to the chapter, then render each comment component individually.
Again, because the database is normalised with normalizr, I really only get the basic content and the user ID of the comment author.
Now, to render something as simple as an author badge, I need to use another connected component to fetch the user details from the user ID I have (both when rendering the chapter author and for each individual comment author).
The component would be something simple like this:
#connect(
createSelector(
(state) => state.entities.get('users'),
(state,props) => props.id,
(users,id) => ( { user:users.get(id)})
)
)
class User extends Component {
render() {
const { user } = this.props
if (!user)
return null
return <div className='user'>
<Avatar name={`${user.first_name} ${user.last_name}`} size={24} round={true} />
</div>
}
}
User.propTypes = {
id : PropTypes.string.isRequired
}
export default User
And it seemingly works fine.
I've tried to do the opposite and de-normalise the data back at a higher level so that for example chapter data would embed the user data directly, rather than just the user ID, and pass it on directly to User – but that only seemed to just make really complicated selectors, and because my data is immutable, it just re-creates objects every time.
So, my question is, is having leaf-like component (like User above) connect() to the store to render a sign of anti-pattern?
Am I doing the right thing, or looking at this the wrong way?
I think your intuition is correct. Nothing wrong with connecting components at any level (including leaf nodes), as long as the API makes sense -- that is, given some props you can reason about the output of the component.
The notion of smart vs dumb components is a bit outdated. Rather, it is better to think about connected vs unconnected components. When considering whether you create a connected vs unconnected components, there are a few things to consider.
Module boundaries
If you divided your app into smaller modules, it is usually better to constrain their interactions to a small API surface. For example, say that users and comments are in separate modules, then I would say it makes more sense for <Comment> component to use a connected <User id={comment.userId}/> component rather than having it grab the user data out itself.
Single Responsibility Principle
A connected component that has too much responsibility is a code smell. For example, the <Comment> component's responsibility can be to grab comment data, and render it, and handle user interaction (with the comment) in the form of action dispatches. If it needs to handle grabbing user data, and handling interactions with user module, then it is doing too much. It is better to delegate related responsibilities to another connected component.
This is also known as the "fat-controller" problem.
Performance
By having a big connected component at the top that passes data down, it actually negatively impacts performance. This is because each state change will update the top-level reference, then each component will get re-rendered, and React will need to perform reconciliation for all the components.
Redux optimizes connected components by assuming they are pure (i.e. if prop references are the same, then skip re-render). If you connect the leaf nodes, then a change in state will only re-render affected leaf nodes -- skipping a lot of reconciliation. This can be seen in action here: https://github.com/mweststrate/redux-todomvc/blob/master/components/TodoItem.js
Reuse and testability
The last thing I want to mention is reuse and testing. A connected component is not reusable if you need to 1) connect it to another part of the state atom, 2) pass in the data directly (e.g. I already have user data, so I just want a pure render). In the same token, connected components are harder to test because you need to setup their environment first before you can render them (e.g. create store, pass store to <Provider>, etc.).
This problem can be mitigated by exporting both connected and unconnected components in places where they make sense.
export const Comment = ({ comment }) => (
<p>
<User id={comment.userId}/>
{ comment.text }
</p>
)
export default connect((state, props) => ({
comment: state.comments[props.id]
}))(Comment)
// later on...
import Comment, { Comment as Unconnected } from './comment'
I agree with #Kevin He's answer that it's not really an anti-pattern, but there are usually better approaches that make your data flow easier to trace.
To accomplish what you're going for without connecting your leaf-like components, you can adjust your selectors to fetch more complete sets of data. For instance, for your <Chapter/> container component, you could use the following:
export const createChapterDataSelector = () => {
const chapterCommentsSelector = createSelector(
(state) => state.entities.get('comments'),
(state, props) => props.id,
(comments, chapterId) => comments.filter((comment) => comment.get('chapterID') === chapterId)
)
return createSelector(
(state, props) => state.entities.getIn(['chapters', props.id]),
(state) => state.entities.get('users'),
chapterCommentsSelector,
(chapter, users, chapterComments) => I.Map({
title: chapter.get('title'),
content: chapter.get('content')
author: users.get(chapter.get('author')),
comments: chapterComments.map((comment) => I.Map({
content: comment.get('content')
author: users.get(comment.get('author'))
}))
})
)
}
This example uses a function that returns a selector specifically for a given Chapter ID so that each <Chapter /> component gets its own memoized selector, in case you have more than one. (Multiple different <Chapter /> components sharing the same selector would wreck the memoization). I've also split chapterCommentsSelector into a separate reselect selector so that it will be memoized, because it transforms (filters, in this case) the data from the state.
In your <Chapter /> component, you can call createChapterDataSelector(), which will give you a selector that provides an Immutable Map containing all of the data you'll need for that <Chapter /> and all of its descendants. Then you can simply pass the props down normally.
Two major benefits of passing props the normal React way are traceable data flow and component reusability. A <Comment /> component that gets passed 'content', 'authorName', and 'authorAvatar' props to render is easy to understand and use. You can use that anywhere in your app that you want to display a comment. Imagine that your app shows a preview of a comment as it's being written. With a "dumb" component, this is trivial. But if your component requires a matching entity in your Redux store, that's a problem because that comment may not exist in the store yet if it's still being written.
However, there may come a time when it makes more sense to connect() components farther down the line. One strong case for this would be if you find that you're passing a ton of props through middle-man components that don't need them, just to get them to their final destination.
From the Redux docs:
Try to keep your presentation components separate. Create container
components by connecting them when it’s convenient. Whenever you feel
like you’re duplicating code in parent components to provide data for
same kinds of children, time to extract a container. Generally as soon
as you feel a parent knows too much about “personal” data or actions
of its children, time to extract a container. In general, try to find
a balance between understandable data flow and areas of responsibility
with your components.
The recommended approach seems to be to start with fewer connected container components, and then only extract more containers when you need to.
Redux suggests that you only connect your upper-level containers to the store. You can pass every props you want for leaves from containers. In this way, it is more easier to trace the data flow.
This is just a personal preference thing, there is nothing wrong to connect leaf-like component to the store, it just adds some complexity to your data flow, thus increase the difficulty to debug.
If you find out that in your app, it is much easier to connect a leaf-like component to the store, then I suggest do it. But it shouldn't happen very often.

Categories

Resources