Replacing HOCs with hooks in tubular
By Alejandro Ocampo (@kadosh) - 06 Aug 2019
tubular-react original implementation
Introduction
If you don’t know what tubular-react is, it is a Data Grid component that uses Material UI to display tabular data. And, our original implementation consisted of the following main components:
- Data Grid component (Main component)
- Data Source (Which was provided in the form of a HOC and we call it
SpecificDataSource
) - Somebody responsible for state and interactions on the Data Grid (
BaseDataSource
) - Other feature components like: Paginators, Toolbars, etc.
But for now let’s focus on the first three components: DataGrid
, SpecificDataSource
and BaseDataSource
.
Our DataGrid
component required a SpecificDataSource
that was intended to do one thing: getAllRecords
, that is a function that deals with the details on how to retrieve the data, either from an array or from a remote data source. And all the logic to deal with state and interactions with the grid lived inside BaseDataSource
. Right from here you can see a design issue: why our state and interactions (API) for the DataGrid
live not on the DataGrid
but in the BaseDataSource
? Because reasons…
Anyway, a simple data grid with our original approach looked like:
const LocalDataGrid = withRemoteDataSource(
() => {
return
<DataGrid />
},
columns,
"https://tubular.azurewebsites.net/api/orders/paged"
);
}
You can notice some interesting things:
- Columns are passed to the
DataSource
DataGrid
will execute some internal magic to access those Columns and the rest of the state and API methods as well.- Implementation looks weird, I just need a DataGrid, why that complexity?
So, that was the point when I started thinking, why can’t I have something like this?:
<DataGrid
columns={columns}
dataSource='https://tubular.azurewebsites.net/api/orders/paged'
/>
Ain’t that better? I mean, if tubular already provides functionality to work either with a local data source or with a remote one, why should the implementation contain details about it?
So, that was my initial step on this trip.
Refactoring
The idea was good, but what about reality? By that time, our DataGrid
render method looked like this:
return (
<DataGridProvider
toolbarOptions={toolbarOptions}
gridName={gridName}
storage={storage}
>
<Paper className={classes.root}>
<GridToolbar>
{children}
</GridToolbar>
<div className={classes.progress}>
{state.isLoading && <LinearProgress />}
</div>
<DataGridTable
bodyRenderer={bodyRenderer}
footerRenderer={footerRenderer}
onRowClick={onRowClick}
/>
</Paper>
</DataGridProvider>
);
These were just bad news to my new approach. Tubular-react had its state spread across multiple locations. Some state was living on the DataSourceContextProvider
context and some other on the DataGridProvider
. Why? Because reasons…
So, time to refactor came… Let’s think about this component from scratch, I need:
- A simple
DataGrid
component - A common state for the entire grid
- An API (grid behavior) to be shared with paginator, filtering and the rest of the grid features
Power of hooks
DataSourceContextProvider
and DataGridProvider
were holding state and actions. I could simply merged those into a single one. Then I wondered: can I put this inside hooks? What if I could simply do useDataGrid()
and just with that I could have the state of my grid and the API to interact with it?
I’m not against the use of Context (as long as it is used properly), but I’m not a big fan of creating components to simply share some logic or data. So, that was the exact point when useDataGrid()
came to life.
useDataGrid(columns, config, dataSource)
I simply did this:
- Moved the entire state and behavior from
DataSourceContextProvider
andDataGridProvider
into myuseDataGrid
hook (read about custom hooks). - Returned an object containing both state and api.
Something really nice with hooks
One of the things I did while moving that logic and state into my custom hook was refactoring the way the grid is refreshed. Previously, every method that require a refresh on the grid had to manually call retrieveData
internal function. For example, the sortColumn
method had to do changes in state and then call retrieveData
to effectively reflect the changes on the grid. That was applied to every method requiring a refresh on the grid.
But what if I told you useEffect can do that for you?
This is great, instead of calling the method to update the grid in a lot of places, I ended up with something like this:
React.useEffect(() => {
api.processRequest();
}, [getColumns, getPage, getSearchText, getItemsPerPage]);
Any state that requires a refresh on the grid is simply added as a dependency on the useEffect and whenever one of those changes, the grid will automatically refreshed. You can read more about that at https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects.
Final implementation
In the end, I was able to successfully implement useDataGrid
hook as a replacement for the two providers tubular-react used to have. A simple data grid with local data source now look like this:
<DataGrid
columns={columns}
dataSource={localData}
gridName='LocalDataGrid'
/>
Where dataSource
prop is simply an array of data. And if your data is coming from a remote source, then you just need to do something like:
<DataGrid
columns={columns}
dataSource='https://somewhere-with-tubular-backend'
gridName='LocalDataGrid'
/>
That’s it, no more HOCs, just a simple DataGrid
component with some props.