Reactivated provides you with an API to make working with Django and React easier.
reactivated.template
Use the template
decorator to define your template structure. By convention, these
templates go in templates.py
of the corresponding app. Simply import NamedTuple
and
template
then define the context.
from typing import NamedTuple from reactivated import template @template class MyTemplate(NamedTuple): name: str title: str age: int location: str
From a standard Django view, render your template by instantiating it and calling
render
on it.
from django.http import HttpRequest, HttpResponse from . import templates def my_view(request: HttpRequest) -> HttpResponse: return templates.MyTemplate( name="George Washington", title="President", age=67, location="Virginia", ).render(request)
Note: Reactivated will look for a default export from
BASE_DIR/client/templates/TEMPLATE_NAME.tsx
reactivated.Pick
Passing model instances to a React template is tricky. We need to serialize the model instance, but for many reasons, can't simply send over every field. Instead, we explicitly tell our template what fields to pick from the model.
Using the following models:
class Author(models.Model): name = models.CharField(max_length=100) age = models.IntegerField() class Book(models.Model): author = models.ForeignKey(Author) title = models.CharField(max_length=100)
We can use Pick
as follows:
from reactivated import Pick, template from typing import Literal from . import models @template class BookDetail(NamedTuple): book: Pick[models.Book, Literal["name", "author.name", "author.age"]]
To use a list of book instances, just wrap everything with List
from typing
:
@template class BookDetail(NamedTuple): book: Pick[models.Book, Literal["name", "author.name", "author.age"]] related_books: List[Pick[models.Book, Literal["name", "author.name", "author.age"]]]
You'll notice we're repeating ourselves quite a bit. We can alias our Pick
and reuse
it:
Book = Pick[models.Book, Literal["name", "author.name", "author.age"]] @template class BookDetail(NamedTuple): book: Book related_books: List[Book}
We would then render the template as follows:
def book_detail(request: HttpRequest, *, book_id: int) -> HttpResponse: book = get_object_or_404(models.Book, id=book_id) return BookDetail( book=book, related_books=list(book.related_books.all()), ).render(request)
reactivated.interface
This behaves identically to the template
decorator. But unlike template
, it will not
expect you to create a corresponding .tsx
file.
This is useful for creating AJAX-only endpoints and statically typing them.
See the AJAX concepts for more information.
templates
When you use the reactivated.template
decorator in your Django code, Reactivated will
generate types for you.
For a template named MyTemplate
inside server/custom_app/templates.py
, you would
then create a file named client/templates/MyTemplate.tsx
and import templates
like
so:
import {templates} from "@reactivated"; export const Template = (props: templates.MyTemplate) => ( <div>{props.properties_of_my_template}</div> );
If you mismatch types, say templates.MyOtherTemplate
and they are
structurally
different, TypeScript will complain. If you don't create the template file or don't
export the template correctly, TypeScript will also complain.
Form
Just like Django templates, you can import Form
and render a basic form as p
tags or
a table. Just like Django's renderer, the outer form
tag is not included. Same goes
for the table
tag.
import {CSRFToken, Form, templates} from "@reactivated"; export default (props: templates.MyFormTemplate) => ( <div> <form method="POST"> <CSRFToken /> <Form form={props.form} as="p" /> <button type="submit">Submit</button> </form> <form method="POST"> <CSRFToken /> <table> <tbody> <Form form={props.form} as="table" /> </tbody> </table> <button type="submit">Submit</button> </form> </div> );
useForm
You'll probably want far more control over the rendering of your form. And if you have
any custom form widgets, the built-in Form
tag will complain as it does not know how
to render them.
The useForm
hook exposes values, errors, fields and more. This gives you full control
over the output.
Because you have access to form.values
, this also allows you to manipulate the form
dynamically.
import {CSRFToken, Widget, useForm, FieldHandler, templates} from "@reactivated"; const Field = (props: {field: FieldHandler}) => { const {field} = props; const widget = field.tag == "custom_app.widgets.CustomWidget" ? ( <CustomWidget field={field} /> ) : ( <Widget field={field} /> ); return ( <div> <label> <div>{field.label}</div> {widget} </label> </div> ); }; export default (props: templates.MyFormTemplate) => { const form = useForm({form: props.form}); return ( <form method="POST"> <CSRFToken /> {form.nonFieldErrors?.map((error, index) => ( <div key={index}>{error}</div> ))} {form.hiddenFields.map((field, index) => ( <Widget key={index} field={field} /> ))} <Field field={form.fields.username} /> <Field field={form.fields.password} /> <Field field={form.fields.country} /> {form.values.country === "USA" && <Field field={form.fields.zip_code} />} </form> ); };
Notice we declare a custom field component using the FieldHandler
convenience type.
The type system will force us to handle custom widgets before delegating to the
built-in Widget
component. In most cases, you'll end up providing custom widget markup
even for built-in widgets, but the Widget
component helps you get started.
FormSet
Just like the Form
tag, you can use the FormSet
component to quickly prototype your
form sets.
import {CSRFToken, FormSet, templates} from "@reactivated"; export default (props: templates.MyFormSetTemplate) => ( <div> <form method="POST"> <CSRFToken /> <table> <tbody> <FormSet formSet={props.formSet} as="table" /> </tbody> </table> <button type="submit">Submit</button> </form> </div> );
useFormSet
For more control over form sets, you can use the useFormSet
hook. This will expose
each form in the form set under forms
. From then on, you can render the forms manually
as with the useForm
example. But don't forget ManagementForm
.
import {CSRFToken, useFormSet, ManagementForm, templates} from "@reactivated"; export default (props: templates.MyFormSetTemplate) => { const formSet = useFormSet({formSet: props.formSet}); return ( <form method="POST"> <CSRFToken /> <ManagementForm formSet={props.formSet} /> {formSet.forms.map((form) => ( // Render each form ))} <button type="button" onClick={formSet.addForm} /> </form> ); };
Note that for convenience, useFormSet
also exposes an addForm
method to dynamically
add a form to the form set.
Just like useForm
, useFormSet
exposes the values of each form under values
.
reverse
Yes, it's magic. You can use reverse
just like you can in your Django code.
All your named views will be there with full static types.
Take the following urls.py
file:
from django.urls import path from . import views urlpatterns = [ path("", views.home_page, name="home_page"), path("blog/<str:post_slug>/", views.post_detail, name="post_detail"), path("widgets/<int:widget_id>/", views.widget_detail, name="widget_detail"), ]
Reverse will expect the following fully type-safe code:
import {reverse} from "@reactivated"; // No arguments. If you pass any, TypeScript will not compile. reverse("home_page"); // An argument of type string with the name post_slug is expected reverse("post_detail", {post_slug: "you-might-not-need-jwt"}); // An argument of type number with the name widget_id is expected reverse("widget_detail", {widget_id: 3});
When your code executes, the right URLs will be resolved for you.
Warning: We need your thoughts and feedback on the
reverse
API. There are potential security considerations.
Formatting and linting should not occupy your time. Reactivated bundles prettier
,
eslint
, black
, flake8
, and isort
to solve this for you.
There's no configuration, just run the below script to fix all the formatting in your application:
scripts/fix --all
You can also fix an individual file by passing the file, like so:
scripts/fix --file client/templates/MyTemplate.tsx
If you run scripts/fix.sh
without any arguments, it will try to fix everything that
has changed against the main
branch of your repository.
You can test your application, including linting and formatting by running
scripts/test.sh
.
Try running it on the example project to see everything passing. Then try breaking it.
You can test only React code and Django code by using the --client
and --server
flags, respectively.