I have server driven data being displayed in tables on multiple pages.
I currently have the following table actions
pageChange
pageSizeChange
sort
load
loaded
I have filters on some pages which need to trigger the load.
I have multiple entities which use this functionality, which will share most the above logic, but will need separate load functionality defining.
My thought was to have actions which take the table ID as a parameter, and then have a createTableReducer function which would also take this ID and would mount table nodes within entities, similarly to createModelReducer in react-redux-form
How could I trigger the entity specific load actions from my generic actions without using something like redux saga?
I was wondering about creating a higher order component and passing it the load function but I don't think this would solve my issue. I could also call both the change action and the load action from the table component itself, but this doesn't' seem like a nice solution.
So, this is a pretty open-ended question and possibilities for implementing this are almost limitless... but, I'll take a shot at it anyhow. This answer isn't doctrine, it's just one possible way to do it.
Let's start by wishing we could plop our tables wherever we want. To differentiate one table from another, all we have to do is pass a tableId
property that links a Table
to the corresponding data in the store.
<Table tableId='customers' />
<Table tableId='products' />
<Table tableId='orders' />
<Table tableId='transactions' />
Next, let's define a state shape to work with. Each top-level key matches a tableId
we wish to store data for. You can make this shape be whatever you'd like, but I'm providing this so you have to do less mental visualization as I reference things in later code. You can also name the top-level tableId
keys whatever you want, just so long as you reference them consistently.
{
customers: {
page: 0,
sortBy: 'id',
cols: [
{id: 'id', name: 'name'},
{id: 'username', name: 'Username'},
{id: 'first_name', name: 'First Name'},
{id: 'last_name', name: 'Last Name'},
...
],
rows: [
{id: 1, username: 'bob', first_name: 'Bob', last_name: 'Smith', ...},
...
]
},
products: {
...
},
orders: {
...
}
...
}
Now, let's get the hard part out of the way: the Table
container. It's a lot to digest all at once, but don't worry, I'll break down the important bits individually.
containers/Table.js
import React from 'react';
import {connect} from 'react-redux';
import actions as * from './actions/';
const _Table = ({cols, rows, onClickSort, onClickNextPage}) => (
<div>
<table>
<thead>
<tr>
{cols.map((col,key) => (
<Th key={key} onClickSort={onClickSort(col.id)}>{col.name}</Th>
)}
</tr>
</thead>
<tbody>
{rows.map((row,key) => ...}
</tbody>
</table>
<button onClick={onClickNextPage}>Next Page</button>
</div>
);
const Table = connect(
({table}, {tableId}) => ({ // example: if tableId = "customers" ...
cols: table[tableId].cols, // state.table.customers.cols
rows: table[tableId].rows // state.table.customers.rows
}),
(dispatch, {tableId}) => ({
onClickSort: columnId => event => {
dispatch(actions.tableSortColumn(tableId, columnId));
// example: if user clicks 'last_name' column in customers table
// dispatch(actions.tableSortColumn('customers', 'last_name'));
},
onClickNextPage: event => {
dispatch(actions.tableNextPage(tableId))
}
})
)(_Table);
export default Table;
If you only learn one thing from this post, let it be this:
The thing to notice here is that mapStateToProps
and mapDispatchToProps
accepts a second argument called ownProps
.
// did you know you can pass a second arg to these functions ?
const MyContainer = connect({
(state, ownProps) => ...
(dispatch, ownProps) => ...
})(...);
In the container I wrote above, I'm destructuring each of the vars we care about.
// Remember how I used the container ?
// here ownProps = {tableId: "customers"}
<Table tableId="customers" />
Now look at how I used connect
const Table = connect(
// mapStateToProps
({table}, {tableId}) => ...
// table = state.table
// tableId = ownProps.tableId = "customers"
// mapDispatchToProps
(dispatch, {tableId}) => ...
// dispatch = dispatch
// tableId = ownProps.tableId = "customers"
)(_Table);
This way, when we're creating the dispatch handlers for the underlying component, (_Table
), we'll have the tableId
available to us within the handler. It's actually kinda nice that the _Table
component itself doesn't even have to concern itself with the tableId
prop if you don't want it to.
Next notice the way the onClickSort
functions is defined.
onClickSort: columnId => event => {
dispatch(actions.tableSortColumn(tableId, columnId));
}
The _Table
component passes this function to Th
using
<Th key={key} onClickSort={onClickSort(col.id)}>{col.name}</Th>
See how it only sends in the columnId
to the handler here ? Next, we'll see how Th
is the one to send the event
, which finally dispatches the action.
components/Table/Th.js
import React from 'react';
const Th = ({onClickSort, children}) => (
<th>
<a href="#sort" onClickSort={event => {
event.preventDefault();
onClickSort(event);
}}>{children}</a>
</th>
);
export default Th;
It's not necessary to keep the event
if you don't want it, but I figured I'd show you how to attach it in case you wanted to utilize it for something.
Moving along ...
actions/index.js
Your actions file is going to look pretty standard. Notice we provide access to tableId
in each of the table actions.
export const TABLE_SORT_COLUMN = 'TABLE_SORT_COLUMN';
export const TABLE_NEXT_PAGE = 'TABLE_NEXT_PAGE';
export const tableSortColumn = (tableId, columnId) => ({
type: TABLE_SORT_COLUMN, payload: {tableId, columnId}
});
export const tableNextPage = (tableId) => ({
type: TABLE_NEXT_PAGE, payload: {tableId}
});
...
Lastly, your tableReducer
might look something like this. Again, nothing too special here. You'll do a regular switch
on the action type and then update state accordingly. You can affect the appropriate part of the state using action.payload.tableId
. Just remember the state shape I proposed. If you choose a different state shape, you'll have to change this code to match
const defaultState = {
customers: {
page: 0,
sortBy: null,
cols: [],
rows: []
}
};
// deep object assignment for nested objects
const tableAssign = (state, tableId, data) =>
Object.assign({}, state, {
[tableId]: Object.assign({}, state[tableId], data)
});
const tableReducer = (state=defaultState, {type, payload}) => {
switch (type) {
case TABLE_SORT_COLUMN:
return tableAssign(state, payload.tableId, {
sortBy: payload.columnId
});
case TABLE_NEXT_PAGE:
return tableAssign(state, payload.tableId, {
page: state[payload.tableId].page + 1
});
default:
return state;
}
};
Remarks:
I'm not going to go into the async loading of the table data. Such a workflow is already nicely detailed in the redux docs: Async Actions. Your choice of redux-thunk, redux-promise, or redux-saga is up to you. Choose whichever you understand best! The key to implementing TABLE_DATA_FETCH
properly is making sure you dispatch the tableId
(along with any other parameters you need) like I did in the other onClick*
handlers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With