4. Improve the block view¶
Let’s add CSV file parsing.
There are many CSV parsers available for Nodejs, we’ll use Papaparse because it also works in the browser.
We’ll need to add the dependency to the add-on. When using yarn workspaces, the
workflow is a bit different. For our simple use case, we could probably run
yarn add papaparse
inside the src/addons/datatable-tutorial
, but
the correct way is to run this command through the project root.
First run yarn workspaces info
to see the workspaces we have available.
> yarn workspaces info
{
"@plone-collective/datatable-tutorial": {
"location": "src/addons/datatable-tutorial",
"workspaceDependencies": [],
"mismatchedWorkspaceDependencies": []
}
}
To add a dependency to the package, run:
> yarn workspace @plone-collective/datatable-tutorial add papaparse
And finally, the new block code:
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Table } from 'semantic-ui-react';
import csv from 'papaparse';
import { getRawContent } from '@plone-collective/datatable-tutorial/actions';
const DataTableView = ({ data: { file_path } }) => {
const id = file_path?.[0]?.['@id'];
const path = id ? `${id}/@@download` : null;
const dispatch = useDispatch();
const request = useSelector((state) => state.rawdata?.[path]);
const content = request?.data;
React.useEffect(() => {
if (path && !content) dispatch(getRawContent(path));
}, [dispatch, path, content]);
const file_data = React.useMemo(() => {
if (content) {
const res = csv.parse(content, { header: true });
return res;
}
}, [content]);
const fields = file_data?.meta?.fields || [];
return file_data ? (
<Table celled>
<Table.Header>
<Table.Row>
{fields.map((f) => (
<Table.Cell key={f}>{f}</Table.Cell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{file_data.data.map((o, i) => (
<Table.Row key={i}>
{fields.map((f) => (
<Table.Cell>{o[f]}</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
) : (
<div>No data</div>
);
};
export default DataTableView;
Writing components where the useEffect
triggers network calls can be pretty
tricky. According to the rule of hooks, hooks can’t be triggered
conditionally, they always have to be run. For this reason it’s important to
add relevant conditions inside the hook code, so be sure to identify and
prepare a way to tell, from inside the hook, if the network-fetching action
should be dispatched.
4.1. The React HOC Pattern¶
It is a good idea to split the code in generic “code blocks” so that behavior and look are separated. This has many benefits: it makes components easier to write and test, it separates business logic in reusable behaviors, etc.
So, can we abstract the data grabbing logic? Let’s write a simple Higher Order Component (HOC) that does the data grabbing:
const withFileData = (WrappedComponent) => {
return (props) => <WrappedComponent {...props} />;
};
export default withFileData(DataTableView);
And now let’s move the file download and parsing logic to this HOC.
We’ll create the src/hocs/withFileData.js
file:
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import csv from 'papaparse';
import { getRawContent } from '@plone-collective/datatable-tutorial/actions';
const withFileData = (WrappedComponent) => {
return (props) => {
const {
data: { file_path },
} = props;
const id = file_path?.[0]?.['@id'];
const path = id ? `${id}/@@download` : null;
const dispatch = useDispatch();
const request = useSelector((state) => state.rawdata?.[path]);
const content = request?.data;
React.useEffect(() => {
if (path && !request?.loading && !request?.loaded && !content)
dispatch(getRawContent(path));
}, [dispatch, path, content, request?.loaded, request?.loading]);
const file_data = React.useMemo(() => {
if (content) {
const res = csv.parse(content, { header: true });
return res;
}
}, [content]);
return <WrappedComponent file_data={file_data} {...props} />;
};
};
export default withFileData;
This HOC now gets the data from the Redux store using the logic and code we’ve used previously and then simply injects it as a new property to the original wrapped component.
An HOC is a simple function that gets a component and returns another component. For a Python developer, the decorators are a very similar concept. One thing to pay attention, React component names need to be referenced as PascalCase in JSX code.
And now the view component is simple, neat and focused:
import React from 'react';
import { Table } from 'semantic-ui-react';
import { withFileData } from '@plone-collective/datatable-tutorial/hocs';
const DataTableView = ({ file_data }) => {
const fields = file_data?.meta?.fields || [];
return file_data ? (
<Table celled>
<Table.Header>
<Table.Row>
{fields.map((f) => (
<Table.Cell key={f}>{f}</Table.Cell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{file_data.data.map((o, i) => (
<Table.Row key={i}>
{fields.map((f) => (
<Table.Cell>{o[f]}</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
) : (
<div>No data</div>
);
};
export default withFileData(DataTableView);
Note: for the purpose of this tutorial, the withFileData
HOC has been
created a bit simplistic. To make it more generic, we could avoid hard-coding
the field name, by doing something like this:
const withFileData = (getFilePath) => (WrappedComponent) => {
return (props) => {
const file_path = getFilePath(props);
...
And we change how we wrap the DataTableView
to keep the file_path specific
logic local to the DataTable
component
export default withFileData(({ data: { file_path } }) => file_path)(
DataTableView,
);