📝 Tutorial
Early days
Before this becomes a fully fledged package, this project exists as a template you can fork and build from. My advice for getting started is to build ontop of what already exists, and use the existing routes until you understand it, then you can delete it!
Note
This example will work you through making a simple Bookshop app and API.
Add your first view
In src.views
add a new file called books.py
and add the following code:
src/views/books.py | |
---|---|
Then in src.views.__init__
we just need to do a small update as seen on line 2:
This small import will let the RF APP know to include your views file in the application.
At this point you can now visit http://localhost:9000/books/ to see a simple API response.
Turn it into a React page
Let's make this simple API response become something far more interesting - a whole react page!
We only need to make a few changes:
Quite a few interesting things should happen when you save the file - you'll see two brand new files appear in the src/js/templates
directory.
I know you want to inspect them and see what has changed, but for now let us go over the changes in the books.py
file first:
The PageProps
baseclass should be used for all React pages.
This is the response type of your React page, and will be transformed into an equivalent Typescript interface. You can inspect the sibling interface now in src/js/template/books/home.type.ts
:
Notice how the name of the interface has changed? This is because each React page in RF only has one set of initial props for a page load, and this naming convention helps to pick it out.
Here you will see we have updated the Flask view function with a return type, our HomePageProps
, that we wrote earlier.
We have also updated the return line to return an instance of that class.
The one line of code that makes this all magically generate the files is rf.page()
. This decorator inspects all the associated pages when the server is in development mode, and if it can't find a related TypeScript file for it, it will generate it for you.
Here are the files it has generated for us:
src/js/template/books/home.tsx
src/js/template/books.home.type.ts
Notice how RF has intelligently figured out the directory and the file name from the name of the python file and the view function.
Note
Sometimes when a new file is generated you need to run make web
again for the page to load - this is only a problem the first time and we're working to fix that bug.
If you refresh your browser at http://localhost:9000/books/ you should see a react page with the default template information from src/js/template/books/home.tsx
.
Add an API view
React is most useful when we can make concurrent API requests to our application server and return content.
RF manages API views that we can be used in our React pages. Let's make one now.
Add this just the home
view function:
src/views/books.py | |
---|---|
All of RF's API Views must return a subclass of APIResponse
. There are a few generic helpers in src.rf.types
too if you want to return common responses like Forbidden
or NotFound
.
The BookResponse
is the response class we're using for our API. You'll see in src/js/api/types.ts
that this gets transformed into a TypeScript interface:
src/js/api/types.ts | |
---|---|
This all happens because of the decorator function rf.api_get
:
@rf.api_get("/details")
def details() -> BookResponse:
return BookResponse(name="Gideon the Ninth", author="Tamsyn Muir.")
Similar to rf.api
- this decorator will inspect the view functions when in development mode, and generate appropriate TypeScript interfaces for you.
This interface is available in our TypeScript code now, but we don't acutally need to plug it into our APIClient, because RF has done that already for us!
Have a look further down the types.ts
file:
src/js/api/types.ts | |
---|---|
The GET_MAP
has updated to include the request (currently none) and response types for this API endpoint. So how do we use it?
Using the API Client
Let's write some typescript shall we?
Open up src/js/template/books/home.tsx
and make these changes:
Let's go over these changes so they make sense.
useGet
is a React Hook that provides access to all the registered API views in our RF application.
It uses the GET_MAP
to make sure the request and responses remain consistent.
We do not need to provide any inputs to the useGet
function except for the API view we want to call - in this case it will always be the file_name.view_function
pattern. In fact, if you typed the example out letter by letter you would've noticed that it provided autocomplete!
Because RF's React pages use React suspense - it gracefully handles the loading state for you. This means that the page will not render until the API call has returned a response.
Because we don't need to worry about the loading state, we can safely apply the values from the API call into our React page.
APIs with input parameters
Let's grow our API functionality even more by making it require an input value. We'll update the API View first:
src/views/books.py | |
---|---|
The only thing we've changed here is we've added an input parameter name
, which is a str
, and then returned it in our API Response. This is just a simple demonstrative example, but you could imagine making database queries or doing some complex maths here - it's just Python after all!
When you save this, you'll notice a new TypeScript interface will be generated in src/js/api/types.ts
:
This new request interface will be mapped to the GET_MAP
in the same file:
src/js/api/types.ts | |
---|---|
In fact, if you try and refresh the browser at http://localhost:9000/books/ you'll see this take affect immediately - the page will crash! This is because the name
value in the request is a required parameter.
Let's update our React app so we can send input data to our API Client.
src/js/template/books/home.tsx | |
---|---|
For the sake of brevity we've removed a bunch of this code in the tutorial. The only change needed is to pass an object with the required parameters to the API and then the page should load fine. You'll notice as you type of the object it will give you hints as to the expected input parameters.
Magic, eh?
API Post requests
Now that we've built an API that accepts data and returns it, let's build something more functional. How about an HTTP POST API View?
What's the difference between a POST view and a GET view in RF?
- HTTP GET views format the input parameters as query parameters and are great for loading content quickly.
- HTTP POST views receive their inputs as JSON in the HTTP Body and accept much more content.
- HTTP POST views are preferred when we want to make state changes to our application.
First things first, let's add a new View to our API:
src/views/books.py | |
---|---|
This should all be looking very familiar now. The one small difference from the previous API is we are using rf.api_post
instead of rf.api_get
.
Just like before, this will have generated two new interfaces for us:
src/js/api/types.ts | |
---|---|
You will also see the POST_MAP
will have been updated:
src/js/api/types.ts | |
---|---|
Similar to the GET_MAP
, the POST_MAP
allows us to manage the API Client nicely.
Let's add a form that uses this API to our home.tsx
file:
I appreciate that quite a lot of lines have been added here. This may look a bit intimdating if you're not used to React but don't worry it'll all make sense.
Let's break down each section bit by bit.
The usePost
hook behaves very differently to the useGet
hook. This is because we often want to trigger to hook after a state change or button press.
Under the hood, the usePost
hook provides a TanStack Query hook for managing the API request.
interface Book {
name: string
author: string
}
const [allBooks, setAllBooks] = useState<Book[]>([{name: 'First book', author: 'No one'}])
const [formName, setName] = useState<string>('')
const [formAuthor, setAuthor] = useState<string>('')
This code sets ups some state using React's state library. We'll use it for the form and the list on the page.
<button
onClick={() =>
addBook.mutate(
{name: formName, author: formAuthor},
{
onSuccess: response => {
allBooks?.push({name: response.name, author: response.author})
setAllBooks(Array.from(allBooks))
},
onError: () => console.log(addBook.error),
},
)
}
>
Add
</button>
The button
logic is the most interesting part of this change - it is where we trigger our POST API request!
The mutate method is the standard TanStack one, but RF forces the input data to match the BookAddRequest
interface it generated earlier.
The rest of this code follows the standard useMutation
side effects from Tanstack which you can read about here.
When you read the page at http://localhost:9000/books/ you will notice the new form is available. If you enter some content in it and hit "Add" - the UI updates, but if you open your network tab in the developer console (right click -> Inspect -> Network) you will see API requests being issued to the Flask server when you click the button.
Want to see how the input validation works in real time? Set the input fields to be empty and hit enter, and you will see some API errors in the console tab.
Refreshing requests
If you want to refresh a request you can do this using the useInvalidate
hook: