PrayTeam / scriptured-prayer

GNU Affero General Public License v3.0
1 stars 0 forks source link

Assess React Template Prototype #22

Closed kenancasey closed 5 months ago

kenancasey commented 6 months ago

I set up ChatGPT's React template in our Django app. It's in the react-prototype branch.

A few observations:

@asherlloyd and @Soyokaze-42, if you get a chance, I'd love for you to check out the code and see what you think.

I guess my core question is: should we reconsider a REST API and traditional decoupled frontend to address these developer experience and performance concerns or do you think these are manageable?

asherlloyd commented 6 months ago

@kenancasey for mostly server-side rendered pages with React components sprinkled in, I typically set up a separate JS package in the repository for developing the components in isolation. We could use, for example, esbuild to set this up.

kenancasey commented 6 months ago

Please set up an example of this and teach me how to use it (and set it up).

asherlloyd commented 6 months ago

@kenancasey checkout the branch I created for a rudimentary example. To compile the scripts for use with the Django project, simply run yarn build inside the scriptured-prayer-components directory. You can also run yarn dev to listen for changes (no hot module reloading yet!). If you want to view the components in isolation from the Django project, run yarn serve.

kenancasey commented 6 months ago

I'm confused. Why are there 2 roots?

asherlloyd commented 6 months ago

It's due to the inherent differences between SPA and MPA React applications. Consider the following:

// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';

function App(props) {
  return <h1>Hello, world!</h1>;
}

const element = document.getElementById('root');
const root = ReactDOM.createRoot(element);
root.render(<App />);
<!-- index.html -->
<!DOCTYPE html>
<html lang="en-US">
  <head>
    <title>Example</title>
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script src="/index.js"></script>
  </body>
</html>

This works great for a SPA application, since there's exactly one container in the DOM to attach our app to. In our case, however, we are trying to squeeze the benefits out of @Soyokaze-42's Django templates, which are distributed into modular snippets like so:

<!-- prayerusercard_form.html -->
<form method="POST">
  {% csrf_token %}
  <button type="submit" name="prayer_nav" value="prev">{% translate "Previous"%}</button>
  <button type="submit" name="prayer_nav" value="next">{% translate "Next"%}</button>
</form>
<!-- usercard_detail.html -->
<form method="POST">
  {% csrf_token %}
  <table>
    {{ usercardnotes_formset.management_form }}
    {% for form in usercardnotes_formset %}
    {{ form.as_table }}
    {% endfor %}
  </table>
  <button type="submit">{% translate "Save" %}</button>
</form>

Whilst we could absolutely make each of these snippets it's own entire React app, encapsulating all of its child components, with this approach I am instead opting for a solution that prefers "atomic" React components. With this practice in mind, consider an alternate version of usercard_detail.html:

<!-- usercard_detail.html with atomic React components -->
<form method="POST">
  {% csrf_token %}
  <table>
    {{ usercardnotes_formset.management_form }}
    {% for form in usercardnotes_formset %}
    {{ form.as_table }}
    {% endfor %}
  </table>
  <input id="save" type="button" data-value="{% translate "Save" %}" />
  <input id="hide" type="button" data-value="{% translate "Hide" %}" />
</form>

<script src="{% static 'compiled/button.js' %}" root="save,hide"></script>

The above example, albeit somewhat contrived, demonstrates how we might "enhance" or "mix" individual React components into a static html page with maximum reuse.

There are benefits and drawbacks to this solution to be sure. If we decide we would rather go for individual React apps on each page, I'm willing to pack this away in favor of that option. @kenancasey, let me know what you think.

kenancasey commented 6 months ago

Thank you for this thorough response @asherlloyd ! Is this an approach you've seen before or elsewhere? What's a good search term? "Atomic components"? MPA react components?

I've never see the data- attributes used to pass in props. Is that specific to this approach or just a React convention/option I've never seen? Again, I acknowledge that my React knowledge is beginner level and not based on much dev experience (but a lot of YouTube videos).

asherlloyd commented 6 months ago

This is something I wrote specifically for our purposes and is therefore non-standard. See the insertComponent function for details on how the components are injected into the DOM. However, the dataset property of the HTMLElement object is the "official" way to provide read/write access to custom data. The dataset object is passed in directly as the props object found in the example component. This is a "native" way to pass values to HTML tags and allows us to pass in any arrangement of named properties assuming they are prefixed with data-.

// example
const el = document.getElementById('root');
ReactDOM.createRoot(el).render(
  React.createElement(Component, { ...el.dataset })
);
kenancasey commented 6 months ago

One more question @asherlloyd when you have the time: How does this approach work with TypeScript? I'm assuming that's more config on the esbuild side. Also, what are your thoughts on TS vs JS?

asherlloyd commented 6 months ago

@kenancasey correct, there would be some additional configuration needed. I highly recommend we go with TypeScript.