Published on: April 20, 2015
I spent the last few days really thinking about the DataGrid
and how to utilize React in the best way possible. DataGrid
currently works, but if we evaluate individual components, I haven't adhered to keeping these modular and reusable.
Every project, at some point, has to be evaluated and adjusted - so I took the time now to walk through my thought process.
As I started to implement the search functionality, a few items are glaring issues that needed to be addressed:
DataGrid
state object is simply doing too much. Storing data that will change is proper, however it is also storing computed values - which is completely uneccessary - as well as I started to duplicate some state
to make things work.TitleBar
just doesn't make sense. The real component here is SearchBox
, and the markup in TitleBar
can be move up to DataGrid
. This also allows us to use SearchBox
elsewhere.DataGrid
. If I want to reuse the SearchBox
or Pagination
for any other components, I would have to recode several lines, which is unproductive and prone to error. I have to move functionality to the right component, without breaking the top-down data flow.Pagination
requires too many properties, and can be greatly simplified.Maybe you have already spotted some of these issues? If so, good for you! During the coding process it's easy to miss optimization opportunities or learn new ways to do things that change the way you think about your project.
In this case, I initially wanted the lower level component to do very little functionality. IF I were simply distributing the data grid as a pre built component, it works. However, after the refactor, I am able to reuse components with less duplicate code.
Currently the state
for the DataGrid
component looks like this:
/* app.jsx */this.state = {count: props.data.length,data: this.paginateData(startEnd.itemStart, startEnd.itemEnd),displayCount: props.displayCount,itemStart: startEnd.itemStart,itemEnd: startEnd.itemEnd,page: props.page,pageOptions: this.getPageOptions(props.data.length, props.displayCount)};
After giving these a hard look, several of these are just computed values - count
, pageOptions
, itemStart
and itemEnd
really should all be controlled at the time of render, and not stored in state
. Each of these are based on data
, page
, and displayCount
.
If we dig a bit deeper, though, even data
is computed. The prop
data is immutable - so it never changes. The state
data is our paginated data set. It's just a sliced array. If we remove the computed items, our state now looks like this:
/* app.jsx */this.state = {page: props.page,displayCount: props.displayCount,searchTerm: props.searchTerm};
This feels so much cleaner. searchTerm
will come into play later when we get to the search component.
DataGrid
also has several class methods doing many different calculations for the Pagination
component. getStartEnd()
, getPageOptions()
, and paginateData()
methods all update the older state version. They also feel like they go together - sort of as a class.
Since we basically killed our entire state
, we need a place to get it. Remember how I mentioned at time of render()
? Initially, I wanted a function to just build out everything. I ran into some difficulties trying to figure out how, though. Static
methods are not aware of state
or props
, Prototype
methods in the component aren't aware of static
functions.
Enter class PagedData
. A static class in the pagination.jsx
file that performs all of the computations needed (save a couple) to populate the Pagination
component, and we expose it in a static
method. We can then pass in our data set, page and displayCount state
and let the Pagination
component take care of itself a bit more. It will also make the component reusable, without rewriting all of the functions somewhere else.
/* pagination.jsx */class PagedData {constructor(d, o) {var r = {paginatedProps:{}, paginatedData:[]};r.paginatedProps.total = d.length;r.paginatedProps.itemStart = PagedData.getStart(r.paginatedProps.total, o.page, o.displayCount);r.paginatedProps.itemEnd = PagedData.getEnd(r.paginatedProps.total, o.page, o.displayCount);r.paginatedProps.pageOptions = PagedData.getPageOptions(r.paginatedProps.total, o.displayCount);r.paginatedProps.page = o.page;r.paginatedProps.displayCount = o.displayCount;r.paginatedData = PagedData.splitData(d, r.paginatedProps.itemStart, r.paginatedProps.itemEnd);return Object.freeze(r);}static splitData(d, s, e) {return d.slice(s - 1, e);}static getStart(t, p, d) {return (t > 0) ? ((p - 1) * d) + 1 : 0;}static getEnd(c, p, d) {var h = p * d;return (h <= c) ? h : c;}static getPageOptions(t, d) {var o = new Array(Math.ceil(t / d));var i = 0;var a = o.length;while(i < a){o[i] = i+1;i++;}return o;}}
I split up the result to help the owner do it's job, allowing it to pass paginatedProps
and paginatedData
to the proper place. Our new class gets created by calling the following method with Pagination.pageData()
and pass in the data
array and our state
object.
/* app.jsx */render() {var paginated = Pagination.pageData(this.props.data, this.state);.../* pagination.jsx */static pageData(d, o) {return new PagedData(d,o);}
I had to do some clean up for the values in Pagination
, but that was fairly simple, and is in the new code set. One other item that needed cleaning up, is correcting the current page when the displayCountOptions
change. That gets moved to a component lifecycle method.
componentWillUpdate(nextProps) {if (this.props.paginatedProps.total !== nextProps.paginatedProps.total) {this.props.onChange({'page' : 1});}if (nextProps.paginatedProps.displayCount !== this.props.paginatedProps.displayCount) {var i = 1;while(this.props.paginatedProps.itemStart > i * nextProps.paginatedProps.displayCount) { i++; };this.props.onChange({'page' : i});}}
componentWillUpdate
and componentDidUpdate
are pretty powerful when used correctly. I dare you to this.setState()
from one of these... ok I don't mean that. Loops are never fun - so avoid this.
Now to call our component, we simply need this -
<PaginationpaginatedProps={paginated.paginatedProps}onChange={this.handleData}/>
###Handling the Change Events In preparing for search, I originally modified the "handlePagination()" method, to simplify the interaction. Then I realized, the method wasn't even required, and could be bypassed altogether.
/* app.jsx *///beforehandlePagination(setting) {var nextState = _.assign({}, this.state, setting);if (nextState.displayCount != this.state.displayCount) {nextState.pageOptions = this.getPageOptions(this.props.data.length, nextState.displayCount);var that = this;nextState.pageOptions.every(function(option){if (that.state.itemStart < option * nextState.displayCount) {nextState.page = option;return false;} else {return true;}});}nextState = _.assign(nextState, this.getStartEnd(nextState));nextState.data = this.paginateData(nextState.itemStart, nextState.itemEnd);this.setState(nextState);}//after...<PaginationpaginatedProps={paginated.paginatedProps}onChange={this.setState.bind(this)}/>
Now any thing from an onChange
event can be handled the same way - by performing setState
which forces a refresh, calling the PageData
method, and everything is up-to-date. Since my low level components and DataGrid
all play well together - I don't even need the function I originally created. Refactor #FTW!
One last item to cover, then we can move on. When you are looking for properties in a component, you need to make sure those are set in it's owner so those are available. However, if those aren't included you get errors and such - and sometimes that's actually avoidable because the property could have a default value. React allows for defaultProps
to be set up. I created those like this:
DataGrid.defaultProps = {data: [],displayCount: 10,page: 1,searchTerm: ""}
Now when I want to use this component, I can pass in <DataGrid />
and not blow it up. With this method, I could render the DataGrid
without any data, and pass it in later. I can also pass in a page, or search term if I have one stored. This makes our components more flexible to the end user.
I had to. I tried to leave it - but as I got deeper into the search component, I couldn't leave it alone. Some of this comes from a deeper understanding of functional programming, and really separating out functionality into its’ simplest forms. To be perfectly honest, I still feel like I could slim this down even more. I have drastically reduced what's in state, and I've built a more modular set of components. I've also adhered to the top down data flow, by exposing functions in each component for it's owner to call and pass back
The repo that's been refactored also includes the search functionality, which will be in another post very soon.
I've updated my GitHub repo with the source code from this tutorial.
Kelly J Andrews - © 2020