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

@rbxts/react-lifetime-component

v1.1.1

Published

A React util that allows you to delay the component's unmounting to your liking.

Downloads

355

Readme

React Lifetime Component

A React util that allows you to delay the component's unmounting to your liking.

Installation

npm i @rbxts/react-lifetime-component

Usage

Create a LifetimeComponent and add children inside. When some children are removed, they will not be unmounted. But rather, the children can control when it's unmounted.

This supports both components with key or a Map<string, Element> as children (using a Map is recommended).

(do not use intrinsic elements (frame, textlabel) or fragments as children)

The information about the lifetime in injected in the props of the component. (You dont need to do anything to the props and can be used as usual)

Any hooks should get the props from the component first. This info is only injected in the component, You can pass the props further down the tree manually, or share them with a React.Context.

Example

import React, { PropsWithChildren, useEffect, useMemo } from "@rbxts/react";
import { LifetimeComponent, useLifetimeAsync } from "@rbxts/react-lifetime-component";

// create a list of windows (edit this to open windows)
// we are gonna use @rbxts/charm for this
const windowsList = Charm.atom<Map<string, React.Element>>(new Map());

// this renders all the windows
function WindowsRenderer() {
	const windows = useAtom(windowsList);

	const windowsRender = useMemo(() => {
		const toRender: Map<string, React.Element> = new Map();
		windows.forEach((render, key) => {
			// create a window element with the `render` as children
			const element = <Window>{render}</Window>;

			toRender.set(key, element);
		});
		return toRender;
	}, [windows]);

	return <LifetimeComponent>{windowsRender}</LifetimeComponent>;
}

// this is the window component
function Window(props: PropsWithChildren) {
	const [anchor, motion] = useMotion(-0.5); // start outside of view

	useEffect(() => {
		// move the window to the center of the screen on mount
		motion.spring(0.5);
	}, []);

	// useLifetimeAsync will remove the component when the async function resolves
	// remember to pass the props where the info is injected
	useLifetimeAsync(props, () => {
		return Promise.try(() => {
			motion.spring(1.5); // move the window outside of the view again
			task.wait(1); // delay the removal of the component to wait for the spring animation to finish
		});
	});

	const position = anchor.map((x) => UDim2.fromScale(x, 0.5));

	return (
		<frame Position={position} AnchorPoint={new Vector2(0.5, 0.5)} Size={UDim2.fromOffset(200, 200)}>
			{props.children}
		</frame>
	);
}

Hooks

useComponentIsActive

Checks if the component is active in the LifetimeController children list (returns true when it's not inside a LifetimeComponent)

function Window(props: PropsWithChildren) {
	const isActive = useComponentIsActive(props);

	// disable all window interactions when the component is not active
	return <frame Interactable={isActive}>{children}</frame>;
}

useIsLifetimeComponent

Checks if the component was rendered inside of a LifetimeComponent

function Window(props: PropsWithChildren) {
	if (useIsLifetimeComponent(props)) {
		// do something, it's ensured that this will not change,
		// so it's somewhat okay to use conditional hooks here
	}

	return <frame>{children}</frame>;
}

useComponentLifetime

Returns a function to set the time in seconds the component will be alive for.

An initial value can be given allowing you to not use the assign function.

function Window(props: PropsWithChildren) {
	// unmounting will be delayed for 5 seconds
	useComponentLifetime(props, 5);

	// you can instead assign it, but it's more verbose
	const setLifetime = useComponentLifetime(props);

	useEffect(() => {
		setLifetime(5); // set the lifetime to 5 seconds
	}, []);

	return <frame>{children}</frame>;
}

useDeferLifetime

Defers the component unmount until the given number of frames have passed

function Window(props: PropsWithChildren) {
	// the component will be unmounted in the next frame
	useDeferLifetime(props);

	// you can also pass the number of frames to defer the component
	useDeferLifetime(props, 5); // the component will be unmounted in the next 5 frames

	return <frame>{children}</frame>;
}

useLifetimeAsync

Returns a function to set an async function that will run when the component is not active. The component will be removed when the async function resolves or fails.

An initial value can be given allowing you to not use the assign function.

function Window(props: PropsWithChildren) {
	// the component will be removed when the async function resolves
	useLifetimeAsync(props, () => {
		return Promise.try(() => {
			// do something
		});
	});

	// you can instead assign it, but it's more verbose
	const setAsync = useLifetimeAsync(props);

	useEffect(() => {
		setAsync(() => {
			return Promise.try(() => {
				// do something
			});
		});
	}, []);

	return <frame>{children}</frame>;
}

LifetimeComponent CanRecover

LifetimeComponent has a CanRecover prop that determines if a component should be recovered if the a children with the same key is found in the lifetimed components.

Right now, if you add a window, remove it, and add it again with the same key you'd end up with two windows, one of them that has lifetime, and the new one you just added.

If you set CanRecover to true, when you add the new window, the old one will be recovered, and become re-active again. This implies that useIsComponentActive can return true after it returned false, and that the unmounting hooks can be cancelled.

So for the Window component example, you may rewrite it like this to support CanRecover:

function Window(props: PropsWithChildren) {
	const isActive = useComponentIsActive(props);
	const [anchor, motion] = useMotion(-0.5); // start outside of view

	useEffect(() => {
		if (isActive) {
			motion.spring(0.5); // move the window to the center of the screen
		} else {
			motion.spring(1.5); // move the window outside of the view again
		}
	}, [isActive]);

	useComponentLifetime(props, 1); // use the lifetime, rather than an async function

	const position = anchor.map((x) => UDim2.fromScale(x, 0.5));

	return (
		<frame Position={position} AnchorPoint={new Vector2(0.5, 0.5)} Size={UDim2.fromOffset(200, 200)}>
			{props.children}
		</frame>
	);
}

SanitizeProps

SanitizeProps is a helper function that removes the injection from the props passed to the component.

You usually dont need to use this unless you use the spread operator, or iterate over the props. This returns a new object without mutating the original props.

function Window(props: PropsWithChildren) {
	return <frame {...SanitizeProps(props)}>{props.children}</frame>;
}

Caveats

  • You cannot use anything that is not a React Component, so adding a <frame /> or a <React.Fragment /> might not work. LifetimeComponent returns a React.Fragment so you can add non-components outside of the LifetimeComponent.

  • This touches some react internals, so it's hacky and might break in the future (but it's been tested).

  • This uses the props to access the Lifetime Controller, so expect extra keys in the props (it uses newproxy() so it's a unique key).

  • Not using any of the hooks for control unmounting will cause the component to never be removed (unless the parent component is removed).

  • Do not combine unmounting hooks, the will conflict with each other and the behavior is unknown.

  • The component is still rendering in the tree when the lifetime is still active, use useComponentIsActive to cancel any action that should only happen when the component is active.

example:

function Window(props: PropsWithChildren) {
	const isActive = useComponentIsActive(props);

	const DoSomething = useCallback(() => {
		if (!isActive) return;
		// do something
	}, [isActive]);

	return (
		<frame>
			<textbutton Event={{ Activated: DoSomething }} />
		</frame>
	);
}