npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@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 like onClick.

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 the useParams() 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 the to 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 the from 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",
})