1.1.1 • Published 11 months ago

@rbxts/react-lifetime-component v1.1.1

Weekly downloads
-
License
ISC
Repository
-
Last release
11 months ago

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>
	);
}
1.1.1

11 months ago

1.1.0

11 months ago

1.0.0

11 months ago

0.0.2

11 months ago

0.0.1

11 months ago