JSPM

  • Created
  • Published
  • Downloads 7
  • Score
    100M100P100Q89880F
  • License MIT

Bridge the gap between React and AdonisJS

Package Exports

  • @microeinhundert/radonis
  • @microeinhundert/radonis/build/src/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@microeinhundert/radonis) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Radonis

Easily bridge the gap between your React frontend and AdonisJS backend. Get DX similar to Remix while having the power of AdonisJS at your fingertips.

Features:

  • Render React views directly from AdonisJS routes and controllers
  • Partially hydrate only the components that require interactivity on the client (Islands Architecture)
  • Includes pre-made hooks for working with AdonisJS inside your React views, both on client and server
  • Styling with Twind built in

Requirements:

  • AdonisJS v5.X.X
  • React v18.X.X
  • ReactDOM v18.X.X

Getting Started

1. Install the packages

Install the client as well as the server package from your command line:

npm install --save @microeinhundert/radonis @microeinhundert/radonis-server

or

yarn add @microeinhundert/radonis @microeinhundert/radonis-server 

2. Configure the server package

node ace configure @microeinhundert/radonis-server

3. Configure i18n (optional)

If you plan to use i18n functionality, first install the official @adonisjs/i18n package:

npm install --save @adonisjs/i18n

Inside the DetectUserLocale middleware, add the following below the switchLocale call:

ctx.radonis.shareTranslations(language, I18n.getTranslationsFor(language));

This makes sure Radonis knows about the available translations as well as the current locale.

4. Configure session storage (optional)

If you plan to use session functionality, install the official @adonisjs/session package:

npm install --save @adonisjs/session

Without it, useSession and useFlashMessages hooks won't work.

Server-Side Templating

Instead of Edge, Radonis uses React to render views on the server. This makes it possible to use the same templating language on both server and client.

Usage in controllers:

import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import { Index, Show } from '../../../resources/views/Users.tsx'; // Where you put your views and how you structure them is completely your choice

export default class UsersController {
  public index({ radonis }: HttpContextContract) {
    return radonis.render(Index);
  }

  public show({ radonis }: HttpContextContract) {
    return radonis.render(Show);
  }
}

Usage in routes:

import Route from '@ioc:Adonis/Core/Route';
import { SignUp } from '../resources/views/Auth.tsx';

Route.get('/signUp', async ({ radonis }) => {
  return radonis.render(SignUp);
});

Using Client-Side Hydration

Radonis uses partial hydration to only hydrate what is needed. In order for Radonis to know what to hydrate on the client, wrap the individual components with the HydrationRoot component:

import { HydrationRoot } from '@ioc:Adonis/Addons/Radonis';

function ServerRenderedComponent() {
  return (
    <HydrationRoot componentName="SomeInteractiveComponent">
      <SomeInteractiveComponent someProp="test">
        This component will be hydrated client-side
      </SomeInteractiveComponent>
    </HydrationRoot>
  );
}

Make sure to only pass a single child to a HydrationRoot component. If you want to hydrate multiple parts of your application, use multiple HydrationRoots instead. Theres another gotcha: Because a HydrationRoot component acts as root for a React instance, HydrationRoots cannot be nested. Also make sure all props passed to a hydrated component are serializable.

Then in your client bundle:

import { initClient } from '@microeinhundert/radonis';
import { lazy } from 'react';

const client = initClient();

// The variable name must match the componentName passed to the HydrationRoot
const SomeInteractiveComponent = lazy(() => import('./components/SomeInteractiveComponent'));

client.hydrate({ SomeInteractiveComponent });

Please note that hydration will take place only when the component is in view.

Hooks

useHydration (Server and client)

import { useHydration } from '@microeinhundert/radonis';

const hydration = useHydration();

// Get info about the HydrationRoot the component is a child of:
console.log(hydration); // => `{ hydrated: false, root: ':Rl6:', componentName: 'SomeInteractiveComponent', propsHash: 'cf5aff6dac00648098a9' }`

// By combining useHydration and useManifest, you can get the props of the component 
// passed to the HydrationRoot from any component in the tree:
const hydration = useHydration();
const manifest = useManifest();

console.log(manifest.props[hydration.propsHash]); // => `{ someProp: 'test' }`

useHydrated (Server and client)

import { useHydrated } from '@microeinhundert/radonis';

const hydrated = useHydrated();

console.log(hydrated); // => `true` if it was hydrated or `false` if not

This hook allows checking if a component was hydrated. Useful for progressive enhancement by showing parts of a component only after hydration.

useI18n (Server and client)

import { useI18n } from '@microeinhundert/radonis';

const i18n = useI18n();

// Get a translated message:
console.log(i18n.formatMessage('auth.signUpTitle')); // => `Some message defined in translations`

This hook also allows formatting via the ICU format, just like the AdonisJS i18n package. Refer to the official AdonisJS Docs for more information about the available formatting rules.

Please note that this hook requires some manual setup.

useManifest (Server and client)

import { useManifest } from '@microeinhundert/radonis';

const manifest = useManifest();

// Get the manifest:
console.log(manifest); // => `{ props: {}, route: {}, routes: {}, locale: 'en', messages: {}, flashMessages: {} }`

Please note that the manifest differs between server-side rendering and client-side hydration, therefore don't use this hook inside of components you plan to hydrate on the client. On the client the manifest only includes data actually needed for client-side hydration.

useRoute (Server and client)

import { useRoute } from '@microeinhundert/radonis';

const route = useRoute();

// Get the current route:
console.log(route.current); // => `{ name: 'users.show', pattern: '/users/:id' }`

// Check if a route is the current route:
console.log(route.isCurrent('users.show'));  // => `true` if currently on `users.show` or a child of `users.show`, `false` if not

// Check if exact match:
console.log(route.isCurrent('users.show', true));  // => `true` if currently on `users.show`, `false` if not

useRoutes (Server and client)

import { useRoutes } from '@microeinhundert/radonis';

const routes = useRoutes();

// Get all routes as object:
console.log(routes); // => `{ 'drive.local.serve': '/uploads/*', ... }`

useUrlBuilder (Server and client)

import { useUrlBuilder } from '@microeinhundert/radonis';

const urlBuilder = useUrlBuilder();

// Build the URL for a named route:
const url = urlBuilder.make('signUp'); // => `/signUp`

// Build the URL for a controller:
const url = urlBuilder.make('users.index'); // => `/users`

// Build the URL with params:
const url = urlBuilder.withParams({ id: 1 }).make('users.show'); // => `/users/1`

// You can also provide path params as an array and they will be populated according to their order:
const url = urlBuilder.withParams([1]).make('users.show'); // => `/users/1`

// You can also provide query params:
const url = urlBuilder.withQueryParams({ cool: ['adonis', 'react'] }).make('tech.index'); // => `/tech?cool=adonis,react

useFlashMessages (Server and client, requires @adonisjs/session)

import { useFlashMessages } from '@microeinhundert/radonis';

const flashMessages = useFlashMessages();

// Check if a flash message exists:
console.log(flashMessages.has('errors.fieldName.0')); // => `true` or `false`

// Get a flash message:
console.log(flashMessages.get('errors.fieldName.0')); // => `required validation failed on fieldName`

// You can also omit the index to automatically get the first item if an array:
console.log(flashMessages.get('errors.fieldName')); // => same as `errors.fieldName.0`

// You can also get validation errors like this:
console.log(flashMessages.getValidationError('fieldName')); // => same as `errors.fieldName`

// Get all flash messages:
console.log(flashMessages.all()); // => `{ 'errors.fieldName.0': 'required validation failed on fieldName', ... }`

The following hooks align with AdonisJS functionality, refer to the official AdonisJS Docs for usage:

useRadonis (Server only)

Returns info about the AdonisJS instance in the following format:

interface RadonisContextContract {
  application: ApplicationContract;
  ctx: HttpContextContract;
  request: RequestContract;
  router: RouterContract;
}

import { useRadonis } from '@ioc:Adonis/Addons/Radonis';

const radonis = useRadonis();

useApplication (Server only)

Returns the AdonisJS ApplicationContract.

import { useApplication } from '@ioc:Adonis/Addons/Radonis';

const application = useApplication();

useHttpContext (Server only)

Returns the AdonisJS HttpContextContract.

import { useHttpContext } from '@ioc:Adonis/Addons/Radonis';

const httpContext = useHttpContext();

useRequest (Server only)

Returns the AdonisJS RequestContract.

import { useRequest } from '@ioc:Adonis/Addons/Radonis';

const request = useRequest();

useRouter (Server only)

Returns the AdonisJS RouterContract.

import { useRouter } from '@ioc:Adonis/Addons/Radonis';

const router = useRouter();

useSession (Server only, requires @adonisjs/session)

Returns the AdonisJS SessionContract.

import { useSession } from '@ioc:Adonis/Addons/Radonis';

const session = useSession();

License

MIT