nextJs ServerSideProps and rendering the HTML for SEO - reactjs

Here is the issue I am having. I am trying to have the Ag-grid render it's html output using NextJS getServerSideProps. However, when I view the source code, it doesn't appear to have any of the HTML rendered for SEO purposes. If I go ahead and output the "staff" array to a div then the HTML output is viewable in the source code so at least I know the function is working. Is there something I need to do to have AGGridReact render its contents?
export default function Home({ staff }) {
const gridRef = useRef();
const defaultColDef = {
resizable: true,
sortable: true,
};
const [columnDefs] = useState([
{ headerName: 'First Name', field: 'first_name' },
{ headerName: 'Last Name', field: 'last_name' },
{ headerName: 'Job Title', field: 'job_title' },
{ field: 'office' },
{ field: 'email' },
{ field: 'phone' },
]);
return (
<>
<main>
<div style={{ height: '600px' }}>
<AgGridReact
id='staff_grid'
ref={gridRef}
rowData={staff}
defaultColDef={defaultColDef}
columnDefs={columnDefs}
rowSelection={'single'}
style={{ height: '100%', width: '100%' }}
></AgGridReact>
</div>
</main>
</>
);
}
// This gets called on every request
export async function getServerSideProps() {
const staff = [];
for (let id = 1; id <= 3; id++) {
staff.push({
id: id,
first_name: 'first' + id,
last_name: 'last' + id,
email: 'member' + id + '#company.com',
phone: '12345' + id,
office: 'place' + id,
job_title: 'Worker ' + id,
});
}
// Pass data to the page via props
return { props: { staff } };
}

TLDR: Diving deep into the ag-grid's node_modules abyss and into their documentation, I found that their grid component is being injected into the DOM (client-side) once an "AG Grid" wrapper component has been mounted. Therefore, this is a client-side only component.
Debugging
When I request the Next page from Postman, I see an empty div where the grid should be:
But when I request the page from the browser, I see the grid:
An even easier way to determine that this is a client-side only component would be to assign the grid a debug prop:
<AgGridReact
debug
rowData={staff}
columnDefs={columnDefs}
rowSelection="single"
/>
We see AG Grid debug logs in the browser (Notice the Rendered on Client message):
But, we don't see any AG Grid debug logs on the server (Notice the Rendered on Server message):
More investigation
I thought I found a server-side rendering solution via their Row Models, but unfortunately it's not referring to the table being SSR'd, but the data being lazy loaded via dynamically fetching data from a server. My guess as to why this table is client-side only is that AG Grid doesn't use a native table, but instead a bunch of div elements with custom styles to represent a table. Since the server doesn't have a DOM (eg, can't access document nor window), calculating these dynamic styles wouldn't be possible.
Alternatives
If you're creating this table for an enterprise and it's absolutely vital to have this page SSR'd for SEO, then I'd recommend having some sort of bot detection in Next's middleware and within gSSP. Then pass an isBot prop to the component and conditionally render a native table (styling won't matter since it's mainly used for SEO). We do something similar for our web application where search results need to be baked into the page on the server, but can be lazy-loaded client-side for a snappier UX.
Here's a working demo. You can change the User-Agent using your browser's tools or by changing it within the request headers.
A more comprehensive bot list can be found here.
What a user sees:
What a bot sees:

Related

Is it bad practice to access HTML elements by ID in React TypeScript?

I was told at a previous job that I should never access HTML elements directly through means like getElementById in React TypeScript. I'm currently implementing Chart.js. For setting up the chart, I was initially using a useRef hook instead of accessing context, but now it seems like I need to grab the canvas by ID in order to instantiate it properly. I want to know if this is kosher.
I suspect something is wrong with me not using a context, because my chart data doesn't load and throws a console error: "Failed to create chart: can't acquire context from the given item"
useEffect(() => {
chart = new Chart(chartRef.current, {
type: "bar",
data: {
labels: labelsArray.map((label) => {
const date = new Date(label);
// add one because month is 0-indexed
return date.getUTCMonth() + 1 + "/" + date.getUTCDate();
}),
datasets: [
{
data: valuesArray,
backgroundColor: "#1565C0",
borderRadius: 6,
},
],
},
options: {
interaction: { mode: "index" },
onHover: (event, chartElement) => {
const target = event.native.target;
(target as HTMLInputElement).style.cursor = chartElement[0]
? "pointer"
: "default";
},
plugins: {
tooltip: {
mode: "index",
enabled: true,
},
title: {
display: true,
text: "Daily Usage Past 30 Days",
align: "start",
fullSize: true,
font: {
size: 24,
},
padding: {
bottom: 36,
},
},
},
scales: {
x: {
display: false,
},
},
elements: {
line: {
borderJoinStyle: "round",
},
},
},
});
return () => {
chart.destroy();
};
}, [labelsArray, valuesArray]);
and HTML:
<div className="mt-80 ml-12 p-8 shadow-lg border-2 border-slate-100 rounded-xl items-center">
<canvas id="chart" ref={chartRef}></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</div>
Also, per the Chart.js documentation: "To create a chart, we need to instantiate the Chart class. To do this, we need to pass in the node, jQuery instance, or 2d context of the canvas of where we want to draw the chart." Not sure how we would do this with a useRef
Yes,
It is not good practice to access dom elements directly through document API.
Because in react
virtual dom is responsible for painting/ re-rendering the UI.
State updation is the proper way to tell react to trigger re-render.
The flow is state updation -> calculate differences -> find who over is using that state -> grab those components -> re-render only those components.
virtual dom is the source of truth for react to render and update actual DOM.
Now, If you directly access some dom elements and do some operation on it, like updating, react will never know that some change has happened and it will basically break the flow of the react, in which case there will be no reason to use react.js
The flow would be accessing some dom element -> updating -> displaying.
The problem with this approach if react encounters that later what i have in virtual dom is not actual presentation in the actual dom, which will create mess.
That is the reason there is useRef hook to manipulate dom.

How can I access nested object and render it to datatable?

How can I render the user_id information under the product array? I'm trying to render the nested information of user_id such as the username
The DataGrid is working successfully since I render all non-nested information or
non-populated info. But I'm also trying to show the information under user_id
{field: "user_id", headerName: "User Name", width: 250,}
this is the field I used, but it's not working since user_id is an object
Edit
Sample
This is what I received
Datagrid.js
<div className="datatable">
<DataGrid
className="datagrid"
rows={list}
getRowId={(row) => row._id}
columns={columns.concat(actionColumn)}
pageSize={9}
rowsPerPageOptions={[9]}
checkboxSelection
/>
</div>
So you will have to use renderCell() => ReactElement property and return the nested object string. I've updated your code sandbox with the solution and it is now rendering the username correctly.
Below is the code that I've modified in your code sandbox.
{
field: "user_id",
headerName: "User Name",
width: 250,
renderCell: (params) => {
return params.row.user_id.username;
},
},
Here is the link to documentation that you may want to go through.

How to create a dynamic layout in react with fully configurable JSON data

I am writing a react application. A core requirement is that the application be completely dynamic and configurable, including choosing layouts, sections and fields, validation etc.
I have two UI. One is the config UI where the user can select the layout, sections, fields like what type of html component etc. Once this is saved, I get data as JSON where I need to draw the UI. This is my second UI. My concern is how do I structure the components to render the UI with the JSON data. The fields & sections will be same but the layout will be different based on what is been selected in the config UI. Below is the rough JSON schema.
{
title: "Test title",
layout: [
{
name: "layout-a"
},
sectionA: {
name: "breadcrumbs"
field: [
{
name: "test",
value: "test",
type: "text"
}
]
},
sectionB: {
name: "actions"
field: [
{
name: "Create",
value: "Create",
type: "button"
}
]
}
]
}
I was thinking of having a layout component which renders all the children from the JSON. Component looks like below
const Layout = ({ children }) => {
return (
<div>
<div className="container">
<div className="content">{children}</div>
</div>
</div>
);
};
and top level component where we read the config json and based on the layout render the component
<Layout>
{viewToShow === "layoutA" && <LayoutA data={config.sections} />}
{viewToShow === "layoutB" && <LayoutB data={config.sections} />}
</Layout>
My question is how do I construct the LayoutA, B or C component so that these sections and fields are rendered differently on the UI?
I think your question leaves a lot of unspecified points for us to offer you a proper solution. My advice is to investigate better what the project real needs are and its main goals, then lay out each piece (component) thoroughly checking what should be "configurable" and to which extent, before coming up with any implementation.
Taking your example "as is", my first thought is to wrap your App component into a Context provider, similar to what we'd do to manage themes.
export const layouts = {
layoutA: {
background: '#fff',
sectionWidth: '100%',
},
layoutB: {
background: '#000',
sectionWidth: '50%',
},
};
export const LayoutContext = React.createContext({
layout: layouts.layoutA, // default layout
toggleLayout: () => {},
})
You could then further populate the layouts object with metadata from a database. Supposing changes do not originate from the UI (think Webflow or Wix Editor), you could use a CMS to update the metadata and propagate the changes.
An example usage would be:
function LayoutTogglerButton() {
return (
<LayoutContext.Consumer>
{({ layout, toggleLayout }) => (
<button
onClick={toggleLayout}
style={{ backgroundColor: layout.background }}>
Toggle Layout
</button>
)}
</LayoutContext.Consumer>
)
}
Again, there are a lot of unspecified points on your request for us to be more specific. The request for "an application to be completely dynamic and configurable, including choosing layouts, sections and fields, validation etc" could mean many things.
Examples of more specific questions: How to create dynamic forms in React with functional components? How to create drag and drop dashboard widgets with React? How to live update/customise themes with styled-components?
Perhaps you could be more specific? Cheers
I am researching a possibility to do something similar. An off the bat approach would look somewhat like this: https://codesandbox.io/s/still-sun-cecudh?file=/src/App.js
Then of course, where this the layout object will be generated and where the parsing will take place will dependent on your use case. I am going with context for layout object generation and a dedicated component for object tree traversal.

.push() is creating duplicate table filter dropdowns when page is revisited

I have custom table with a filters property. 3 of the filters are hard coded, but 1 of the filters is gathered through a POST request from our API because their filter options are frequently updated in our database by non-engineering folks.
My issue is that when you hit the back button to get to the page with this custom table, a duplicate Gateways filters is added to the page. This item disappears when you refresh the page, which demonstrated to me that I am running into an issue with the State. How can I make it so the Gateways filter is only displayed once?
Here is the code for the table:
<CredcapTable
defaultSort="created_at"
cols={cols}
filters={filters}
searchFunc={this.searchFunc}
recordMappingFunc={this.recordMappingFunc}
/>
Here is the function that pulls the Gateways filters from the database, which is called in componentDidMount:
private loadFilters = () => {
searchGateways(this.props.app.api, new SearchRequest()).then((res: SearchResults<Gateway>) => {
const opts = new Array<any>();
opts.push({ label: "All", value: "*" });
res.results.map((g: Gateway) => {
opts.push({ label: `${g.name} [${g.code}]`, value: g.id });
});
filters.push({
field: "gateway_id",
label: "Gateway",
options: opts
});
this.setState({ filters });
});
};

How to get Datatable components rerendered upon state changes

I have a react-redux application this has the following code structure, which is running correctly.
class Customers extends Component{
state = {
showAddCustomerForm : false
}
toggleAddCustomerForm = ()=>{
this.setState({showAddCustomerForm: !this.state.showAddCustomerForm})
}
render(){
return (
<Fragment>
<AddCustomerForm />
<Datatable
options={{
data: this.props.customers,
buttons: [
{
extend: 'csv',
text: '<i class="fa fa-file-excel-o"></i>Excel'
},
{
text: 'Add Customer',
action: this.toggleAddCustomerForm
}
],
columns: [
{ data: "name" },
{ data: "email" },
{ data: "mobile", "defaultContent": "<i>Not set</i>" },
{ data: "landline", "defaultContent": "<i>Not set</i>"}
]
}}
filter="true"
className="table table-striped table-bordered"
width="100%"
/>
</Fragment>
)
}
}
The Datatable Component uses the redux store for rendering the table. When I add a new Customer using the AddCustomerForm I update Redux store. This actually changes the application state, So I expect the Datatable Component to RE-Render and show me the new customer in the table. however, this does not happen. If I hit it shows me correctly.
I understand that both React and jQuery Datatables manage DOM independently, but I am sure there must be a way to use datatables in a rerenderable component...
Please help..
For datatables fed by HTML or JavaScript source, I'm afraid, there's no way to re-render datatable upon source data modification.
However, it is rather good news from the standpoint of data consistency across multiple clients who might use your application simultaneously. To maintain that, you may use AJAX requests to update back-end data which all the users refer to and, upon successful update, throw ajax.reload() to sync your client with backend.
If, for some reason, you might wish to update your datatable locally, you may consider cleaning table contents and re-populating it with your data, like that:
//datatable initialization
var datatable = $('#mytable').DataTable({
data: myDataArray
});
//source data modification
myDataArray.push({
attr1: "value",
attr2: "value"...
});
//purge datatable contents
datatable.clear();
//re-populate datatable
$.each(myDataArray, function () {
datatable.row().add(this);
});
//re-render up to date datatable
datatable.draw();
You can update datatable by implementing the "componentDidUpdate" function of your component. Example:
componentDidUpdate=()=>{
//console.log("Component did update");
if (this.props.customers.length>0){
this.createTable()
this.updateTable();
}
};
Then implement the function for updating data. E.g
updateTable=()=>{
let table=$('#mytable').DataTable();
table.clear();
table.rows.add(this.props.consumers).draw();
}
I would also create the table using javascript rather than in the JSX. That way you can destroy the table and create it afresh every time you want to update
createTable=()=>{
if ( $.fn.DataTable.isDataTable( '#mytable' ) ) {
$('#mytable').DataTable().destroy();
}
let table=$('#mytable').DataTable({
"columns":columns,
});
And the JSX would only contain the id of the table
<table id="mytable">

Resources