I’m in the process of converting the front end components of my checklist app from static elements defined in the html to reusable react components.
I feel compelled to point out that this is not strictly necessary as my app is very small and already renders very fast with the server-rendered express views.
However, I’ve had it on my todo list to learn react and this is my easiest opportunity to do that, so why not? Nothing like some unnecessary refactoring 😜.
Plus, there will be a true benefit experienced here in terms of increased ability to make future modifications to the buttons across the site, once they all originate from a shared component. Reusability for the win.
Additionally, the delete button currently successfully deletes the checklist from the database, but does not delete it from the view. Once the button has been updated to a React component we can easily address that with React’s useState hook.
So really, there are two main objectives here:
- Convert the delete button to a react component
- Update the button to visually remove the row on click
This Node project uses Expressjs as the app framework and handlebars as the view engine.
Once I’ve created my React button component, I’ll use it to implement the delete button of the all checklists view:

Currently, the delete button lives in all-checklists.hbs and has the following markup:
<button class="btn btn-danger btn-delete" data-id="{{this.id}}">Delete</button>
The button is inside of an express {{#each}} control, so the this.id being assigned to the data-id attribute is a handlebars variable and refers to each individual checklist as it is iterated over.
{{#each checklists}}
This allows the button to retain its mapping to the specific row as well as the checklist it is being rendered with. This will be useful when we attach functionality to the button and we want to target only that row and it’s contents.
Express Technical Details:
The checklists server-side variable is set in a controller named getAllChecklists, which in turn renders the all checklists view. That same controller then calls the findAll method of a model named Checklist, which retrieves all saved checklists from the database.
The findAll call returns a promise, which is then passed to a then(). Inside .then() the code triggers a render of the all checklists view. The database-retrieved checklists is then passed to that view as the variable checklists.
exports.getAllChecklists = (req, res, next) => {
Checklist.findAll({
raw: true,
}).then((checklists) => {
res.render("admin/all-checklists", {
pageTitle: "Admin Checklists View",
path: "/admin/all-checklists",
checklists: checklists,
layout: "admin-layout",
});
});
};
Now checklists is available to the view.
There is one problem with this.id– it is available to the handlebars view which is rendered on the server, but it will not immediately be available to my client-side react button, which will live on the browser client. I’ll need to find a way to pass that data to the front end.
Hydration Options
I ran into a similar issue for the logged-in state for the header. I was able to include a script on that page that calls localStorage.setItem. I passed a handlebars expression to it checking if the variable loggedIn is true. Since that handlebars expression is evaluated on the server before the view is sent to the browser, the actual value sent with the setItem call is either of the values true or false, depending on the result of the evaluated expression. The boolean variable loggedIn is available to the template because it has been set in a middleware as an Express res.local that is part of every page request.
Maybe even easier than the localStorage options, I could pass the Express variable to the window object for client-side access:
To pass the checklists variable to my client side react component I utilized triple mustache brackets in the handlebars view. I wrapped these in backticks. This allows for the handlebars engine to evaluate that variable’s value at server runtime.
'{{{jsonStringify checklists}}}'
Above you can also see I used a jsonStringify handlebars helper I created to convert the checklists JSON object into a JSON string, which was then safe to send in the response. This conversion also happened at server runtime. The value is now ready to be delivered in the script to the client.
Then I passed that resulting string to JSON.parse, which is not triggered on the server but on the client’s runtime– converting the string back into a JSON object. I then assigned that object to the variable checklists, available to my client script and therefore my react button. Whew!
JSON.parse is part of the script delivered to the client, it’s never run on the server. We only needed the JSON value it would be using to be converted to a string by the server, so that it could be safely delivered to the client.
The entire process I just described in the previous four paragraphs looks like this in code:
const checklists = JSON.parse('{{{jsonStringify checklists}}}');
Now with my checklists data available, I’m ready to use React.createElement to insert my checklistList component:
{{#each checklists}}
<div id="checklist-list-container" class="checklists_list"></div>
{{/each}}
<script type="module">
const checklists = JSON.parse('{{{jsonStringify checklists}}}');
// Render Checklist List Component
const checklistList = ReactDOM.createRoot(document.getElementById('checklist-list-container'));
checklistList.render(
React.createElement(ReactChecklistList, {
customClass: ['test'],
checklists: Array.isArray(checklists) ? checklists : []
}, 'CHECKLIST LIST COMPONENT'));
</script>
Here is how the helpers is defined in my main app file, if you’re curious:
helpers: {
jsonStringify: function(context) {
return JSON.stringify(context);
}
}
This seems like a good time to mention that I created a ReactChecklistList component. It’s actually one of three components that I created for this implementation. The call order goes like this:
ReactChecklistList–>ReactChecklistRow–>ReactButton
The checklist list recieves the checklists variable as a prop and then iterates over it with Array.map to output the individual checklist rows:
export default function ReactChecklistList({ customClass, checklists, children }) {
return checklists.map((checklist) => {
// Ensure items is an array
const items = Array.isArray(checklist.items)
? checklist.items
: (typeof checklist.items === 'string' ? JSON.parse(checklist.items) : []);
return (
<ReactChecklistRow items={items} checklist={checklist} key={checklist.id} />
);
});
}
Then, ReactChecklistRow outputs the markup and and content of each row- title, description, checklist and our delete button:
import ReactButton from '../button/button.js';
import { handleDelete, handleActivate } from '/js/components/button-actions.js';
export default function ReactChecklistRow({ items, checklist }) {
const [isHidden, setIsHidden] = React.useState(false);
const toggleHidden = (checklistId) => {
setIsHidden(!isHidden);
handleDelete(checklistId);
};
return (
<div key={checklist.id} className={`${isHidden ? 'hidden' : ''} checklist-row items`}>
<h3>{checklist.title}</h3>
<p>{checklist.description}</p>
{/* loop through checklist items here */}
<ul className="item list-group">
{items.map((item, index) => (
<li key={index} className="list-group-item">{item}</li>
))}
</ul>
<ReactButton
customClass={['react-button test-class btn btn-danger']}
name="button"
id={`delete-btn-${checklist.id}`}
data-checklist-id={checklist.id}
onClick={() => toggleHidden(checklist.id)}
>Delete
</ReactButton>
<ReactButton
customClass={['react-button test-class btn btn-danger']}
name="button"
id={`activate-btn-${checklist.id}`}
data-checklist-id={checklist.id}
onClick={() => handleActivate(checklist.id)}>Activate
</ReactButton>
</div>
);
}
The checklist row component also contains useState. This state will set the value of the isHidden variable, which is what will determine if the row is visible or hidden.
I’ve written a function named toggleHidden into the row component. This function will be passed to the callback function of the onClick prop of my ReactButton component in the row:
<ReactButton
customClass={['react-button test-class btn btn-danger']}
name="button"
id={`delete-btn-${checklist.id}`}
data-checklist-id={checklist.id}
onClick={() => toggleHidden(checklist.id)}
>Delete
</ReactButton>
Now, when the delete button is clicked, it will will trigger toggleHidden, which will set the isHidden value scoped to only that row, successfully hiding that row:
And that’s it!
Until next time!
Leave a Reply