@react-ion/ssr
v0.1.32
Published
The framework is split into 2 parts. The async core and the server side. The core handles prefetching async resolvers, almost everything is build around this feature.
Downloads
63
Readme
@react-ion/ssr (A server side react framework)
About
The framework is split into 2 parts. The async core and the server side. The core handles prefetching async resolvers, almost everything is build around this feature.
Documentation
Core:
Async
Async.create(resolver: Async.Resolver | Api.Method, component: React.FC, options?: Async.Options): React.FC
:
The first parameter needs to be an function that returns some data.
The second parameter is the React.FC
itself which will render based on the Async.State
. A new React.FC
will be returned with some extra properties such as prefetch
, cache
and invalidator
;
The third parameter is optional and which can set the default prefetch
and cache
props if no props are given to the component.
prefetch
:?boolean
to prefetch the data before rendering the whole app set.cache
:?number | { timeout?: number, prefetch?: boolean }
to auto resolve the resolver when the timeout is reached.invalidator
:?function
to invalidate the resolved data and resolve again. Usefull for events likeonClick
.
The first parameter of the resolver is the props that are given to the component. The second argument is the api object to make calls to the api endpoints. The reason to use the second argument is to use the same session data on the server as the initial request. Otherwise if no session is used the global.api
object can be used.
const AsyncTest = Async.create(() => fetch("some url to fetch").then(res => res.json()), ({ data, error, isLoading, invalidate }) =>
{
if(isLoading)
return "Loading...";
if(error)
{
console.error(error);
return null;
}
// if isLoading and error is undefined that means that data exists!
return (
<div onClick={() => invalidate()}>
text: {data.text}
</div>
);
});
// To use the AsyncTest component
const Test = () => <AsyncTest />;
// Invalidate every 5 seconds
const Test = () => <AsyncTest cache={5000} />;
// Prefetching
const Test = () => <AsyncTest prefetch />;
Async.useInvalidator(): Async.Invalidator
:
A hook to create a invalidate
function which can be called to invalidate the async state.
// with the example from above
const InvalidateTest = () =>
{
const invalidate = Async.useInvalidator();
return (
<div>
<AsyncTest prefetch invalidator={invalidate}>
<button onClick={() => invalidate()}>Invalidate</button>
</div>
);
};
Dynamic
Dynamic.create(importer: Dynamic.Importer)
:
Is usefull if the app needs to be split into seperate chunks. The first parameter needs to be a dynamic import. The second argument can optionally be an Dynamic.Options
object.
The options is usefull for rendering custom error and loading components.
The only prop that get exposed is the prefetch
prop which works the same as the Async
prefetch prop.
// with the example from above
const HomePage = Dynamic.create(() => import("./pages/Home"));
const Test = () => <HomePage prefetch />;
Context
Context.create<T>(defaultValue: T)
:
To keep track of all the contexts for each async component you will need to use Context.create()
instead of the default React.createContext()
function.
Context.create
will expose 2 properties. A Provider
and a use
method.
const CounterContext = Context.create({ count: 1 });
const Counter = () =>
{
const { count } = CounterContext.use();
return <h1>{count}</h1>; // renders 100
}
const App = () =>
{
return (
<CounterContext.Provider value={{ count: 100 }}>
<Counter />
</CounterContext.Provider>
);
};
Context.createAsync(initializer: Async.Resolver | Api.Method)
:
This is a wrapper around the Context.create()
function which allows for
const AuthContext = Context.create(async (_, api) =>
{
const { data } = api.auth.login.get();
return {
isLoggedIn: !!data
};
});
const ControlPanel = () =>
{
const { isLoggedIn } = AuthContext.use();
if(!isLoggedIn)
return null;
return <h1>Yay your logged in!</h1>;
}
const App = () =>
{
return (
<AuthContext.Provider>
<ControlPanel />
</AuthContext.Provider>
);
};
Router
Route:
The <Route />
component will render the Component of children if the path matches the current path.
Props:
- path:
string
the path to match to. The path can contain params like/user/:id
which can be accessed by using theuseParams()
hook. - exact:
?boolean
says that the path needs to match exactly with the current path. - title:
?string
a title to set when the route matches. - withBase:
?boolean
tells to prefix the route with the currents page's base path. This will allow to use the same routes on multiple pages. - Component:
?React.FC
the component to render when the route matches. Otherwise the children of the Route will be renderer.
// Simple example
const App = () => (
<>
<Route path="/home" title="Home" exact>
<h1>Home</h1>
</Route>
<Route path="/about" title="About" exact Component={About} />
</>
);
// The routes can match on multiple pages because of the withBase prop.
// `/admin/auth/login` will match on the `/admin` page.
// `/auth/login` will match on the `/` page.
const AuthRoutes = () => (
<>
<Route withBase exact path="/auth/login" title="Login">
<h1>Login</h1>
</Route>
<Route withBase exact path="/auth/register" title="Register">
<h1>Register</h1>
</Route>
</>
);
Link:
The <Link />
will route to the given to
path and won't reload the whole app. If the Link routes to another page it will load the whole new app.
Props:
- to:
string
the path to match to. - exact:
?boolean
says that the path needs to match exactly with the current path. - activeClass:
?string
a class name to set when theto
path matches current route. - withBase:
?boolean
tells to prefix the route with the currents page's base path. This will allow to use the same routes on multiple pages.
export const Navbar = () => (
<nav>
<Link to="/home">Home</Link>
<Link withBase to="/auth/login">Login</Link>
<Link withBase to="/auth/register">Register</Link>
</nav>
);
Redirect:
The <Redirect />
component will redirect when the from
prop matches the current route.
Props:
- from:
string
the path to match with. - to:
string
the path to redirect to. - exact:
?boolean
says that the path needs to match exactly with the current path. - withBase:
?boolean
tells to prefix thefrom
prop with the currents page's base path. This will allow to use the same routes on multiple pages.
export const RedirectToHome = () => (
<Redirect from="/" to="/home" exact />
);
useParams:
The useParams()
hook will retreive the current routes params.
const User = () =>
{
const { id } = useParams();
if(!id)
return <Redirect from="/user" to="/home" />; // redirect if no id is provided
return (
<div>
Show user with id {id}
</div>
);
}
const App = () => (
<Route path="/user/:id" title="User" exact Component={User} />
);
useTitle:
The useTitle()
hook will set the document title.
const User = () =>
{
const { id } = useParams();
useTitle(id && (title) => `${title} ${id}`); // if id is defined the title will be set to "User {id}".
if(!id)
return <Redirect from="/user" to="/home" />; // redirect if no id is provided
return (
<div>
Show user with id {id}
</div>
);
}
const App = () => (
<Route path="/user/:id" title="User" exact Component={User} />
);
useNavigate:
The useNavigate()
hook retruns a function which can be used to navigate to another page/route.
const App = () =>
{
const navigate = useNavigate();
return (
<div onClick={_ => navigate("/home")}>
Navigate to home!
</div>
);
};
useOnNavigateStart/useOnNavigateEnd:
These hooks will call the callback when the router starts/ends navigating. If an async callback is provided the router will wait till they all are resolved before navigating further.
const App = () =>
{
const [isLoading, setIsLoading] = useState(false);
useOnNavigateStart(async () => { setIsLoading(true); await wait(500); }); // wait for 500ms to allow animations to take time
useOnNavigateEnd(() => setIsLoading(false));
return (
<div>
{isLoading && "Loading..."}
</div>
);
};
Store
To use global states you can use the Store.create
method. Whenever a property changes on the store all components which uses the store will rerender. No setState or other functions are needed to update and rerender the components.
const GlobalStore = Store.create(() => ({ count: 1 }), "GlobalStore");
const App = () =>
{
const store = GlobalStore.use();
return (
<div onClick={_ => store.count++}>{store.count}</div>
);
};
Server:
The server pacakge exposes 2 classes, an Api class and a Model class.
Api:
The Api class is an abstract class which can be used to create api endpoints. When exported from the api entry the whole app can use the endpoint just like a function (on the global/window object).
When called it returns an Api.Response<T>
which contains or an error or the data.
To add middleware use the @Api.middleware()
decorator. It accepts a function with the (req: Request, res: Response)
arguments. To cancel the request throw an Error.
To validate the api props use the @Api.validate()
decorator. It accepts a object with a layout to validate against.
// src/api/users.ts
class Users extends Api
{
// checks if the count property is of type number | string
@Api.validate({ count: ["?number", "?string"] })
override async get({ count = 10 }: { count?: string | number }): Promise<User[]>
{
const response = await fetch(`https://random-data-api.com/api/users/random_user?size=${count}`).then(r => r.json());
if(response.error)
throw new Error(response.error);
return response;
}
@Api.middleware(req => { if(!res.session.user?.isAdmin) throw new Error("Not Authorized!"); })
override async delete({ id }: { id: number })
{
return await User.delete({ where: { id } });
}
}
// src/api.index.ts
export default {
users: Users
};
// now throughout the whole app you can just use the api like:
const { data, error } = await api.users.get();
const { data, error } = api.users.get({ count: 20 });
// it can also directly be used with Async Components like:
const UsersList = Async.create(api.users.get, ({ data, error, isLoading }) =>
{
if(isLoading)
return "Loading...";
if(error)
{
console.error(error);
return null;
}
return (
<>
{data.map(({ id, first_name, last_name }) => (
<div key={id}>
{id} - {first_name} {last_name}
</div>
))}
</>
);
});
const App = () => <UsersList />;
// or
const App = () => <UsersList count={20} />;
Model:
The model class can be used the create database models/tables. The tables will automaticly be generated when not existing.
To register the model use the Model.register(tableName, ?seeder)
function. When a seeder is provided the seeder will run after the table is created.
To defined some column attributes there are some decorators that can be used:
@Model.column
: to create a default table column. (the type will be infered from typescript through reflection.)@Model.default
: to set a default value when an entity is created.@Model.unique
: to make the column unique.@Model.required
: to make the column required.@Model.transform
: to make transform the column data on insert/update or select.
@Model.register("accounts", Account.seed)
export class Account extends Model
{
private static readonly seed = async () =>
{
const data = await fetch(`https://random-data-api.com/api/users/random_user?size=100`).then(r => r.json());
await Promise.allSettled(data.map(async (data: any) =>
{
await Account.create({
email: data.email,
password: data.password,
username: data.username
})
}));
};
private static readonly generateHash = async () =>
{
return createHash("sha512").update(`${Math.random() * Date.now()}`).digest("hex");
};
private static readonly hashPassword = (password: string) =>
{
return createHash("sha512").update(password).digest("hex");
};
@Model.unique
@Model.required
public username!: string;
@Model.unique
@Model.required
public email!: string;
@Model.required
@Model.transform(Account.hashPassword, Model.Insert)
public password!: string;
@Model.default(() => false)
public readonly isActivated?: boolean = false;
@Model.default(Account.generateHash)
public readonly activationHash?: string;
public checkPassword(password: string)
{
return this.password === Account.hashPassword(password);
}
}
// The models can be referenced inside other models
@Model.register("users")
export class User extends Model
{
@Model.column
public firstName?: string;
@Model.column
public lastName?: string;
@Model.column
public address?: string;
@Model.column
public postalCode?: string;
@Model.column
public phone?: string;
@Model.column
public account?: Account; // This will reference the Account model/table
}
// To select models
User.find({
// (optional) tells which columns to retreive, if not provided all columns are retreived.
select: {
firstName: true,
lastName: true
},
// (optional) where case
where: {
firstName: "foo"
},
// or a more complex where case
where: {
id: { ">=": 100, "<": 200 } // select where id >= 100 and id < 200
},
// (optional) tells which referenced models to include in the found models
include: {
account: true
},
// (optional) the max number of models to select
limit: 100,
// (optional) how to sort the models/rows
order: {
firstName: "ASC"
},
// or directly sort on id
order: "ASC",
})