Compare commits

...

2 Commits

Author SHA1 Message Date
hydroflame
9bb0bc5728 2nd commit 2025-08-27 21:59:24 -04:00
hydroflame
80d566f051 First pass at better rerender 2025-08-27 20:24:50 -04:00
18 changed files with 345 additions and 87 deletions

View File

@@ -0,0 +1,12 @@
import React from "react";
import { PlayerObject } from "../../../PersonObjects/Player/PlayerObject";
import { Money } from "../../../ui/React/Money";
import { usePlayerSelector } from "../../../utils/PlayerExternalStore";
const selectHacknetExpenses = (p: PlayerObject) => -p.moneySourceA.hacknet_expenses || 0;
export function HacknetExpenses(): React.ReactElement {
const spent = usePlayerSelector(selectHacknetExpenses);
return <Money key="money" money={spent} />;
}

View File

@@ -0,0 +1,11 @@
import React from "react";
import { PlayerObject } from "../../../PersonObjects/Player/PlayerObject";
import { Money } from "../../../ui/React/Money";
import { usePlayerSelector } from "../../../utils/PlayerExternalStore";
const selectHacknetMoney = (p: PlayerObject) => p.moneySourceA.hacknet;
export function HacknetProduced(): React.ReactElement {
const produced = usePlayerSelector(selectHacknetMoney);
return <Money key="money" money={produced} />;
}

View File

@@ -0,0 +1,16 @@
import React from "react";
import { useCallback } from "react";
import { PlayerObject } from "../../../PersonObjects/Player/PlayerObject";
import { usePlayerSelector } from "../../../utils/PlayerExternalStore";
import { safeGetHacknetServer } from "../utils";
interface IProps {
index: number;
}
export function HacknetServerLevel({ index }: IProps): React.ReactElement {
const level = usePlayerSelector(
useCallback((p: PlayerObject) => safeGetHacknetServer(p, index)?.level ?? "???", [index]),
);
return <>{level}</>;
}

View File

@@ -0,0 +1,34 @@
import React from "react";
import { hasHacknetServers } from "../../../Hacknet/HacknetHelpers";
import { HacknetNode } from "../../../Hacknet/HacknetNode";
import { HacknetServer } from "../../../Hacknet/HacknetServer";
import { PlayerObject } from "../../../PersonObjects/Player/PlayerObject";
import { GetServer } from "../../../Server/AllServers";
import { usePlayerSelector } from "../../../utils/PlayerExternalStore";
import { HashRate } from "../../../ui/React/HashRate";
const selectTotalProduction = (p: PlayerObject) => {
let totalProduction = 0;
for (let i = 0; i < p.hacknetNodes.length; ++i) {
const node = p.hacknetNodes[i];
if (hasHacknetServers()) {
if (node instanceof HacknetNode) throw new Error("node was hacknet node"); // should never happen
const hserver = GetServer(node);
if (!(hserver instanceof HacknetServer)) throw new Error("node was not hacknet server"); // should never happen
if (hserver) {
totalProduction += hserver.hashRate;
} else {
console.warn(`Could not find Hacknet Server object in AllServers map (i=${i})`);
}
} else {
if (typeof node === "string") throw new Error("node was ip string"); // should never happen
totalProduction += node.moneyGainRatePerSecond;
}
}
return totalProduction;
};
export function HashTotalProduction(): React.ReactElement {
const prod = usePlayerSelector(selectTotalProduction);
return <HashRate key="hashRate" hashes={prod} />;
}

View File

@@ -0,0 +1,17 @@
import React from "react";
import { Button, ButtonProps } from "@mui/material";
import { usePlayerSelector } from "../../../utils/PlayerExternalStore";
import { PlayerObject } from "../../../PersonObjects/Player/PlayerObject";
type MoneyButtonProps = {
cost: number;
} & ButtonProps;
export function MoneyButton(props: MoneyButtonProps): React.ReactElement {
const canAfford = usePlayerSelector(React.useCallback((p: PlayerObject) => p.canAfford(props.cost), [props.cost]));
return (
<Button disabled={!canAfford || props.disabled} {...props}>
{props.children}
</Button>
);
}

View File

@@ -0,0 +1,25 @@
import * as React from "react";
import { formatMoney } from "../../../ui/formatNumber";
import type { Theme } from "@mui/material/styles";
import { makeStyles } from "tss-react/mui";
import { usePlayerSelector } from "../../../utils/PlayerExternalStore";
import { PlayerObject } from "../../../PersonObjects/Player/PlayerObject";
const useStyles = makeStyles()((theme: Theme) => ({
unbuyable: {
color: theme.palette.action.disabled,
},
money: {
color: theme.colors.money,
},
}));
interface IProps {
cost: number;
}
export function MoneyCost({ cost }: IProps): React.ReactElement {
const { classes } = useStyles();
const canAfford = usePlayerSelector(React.useCallback((p: PlayerObject) => p.canAfford(cost), [cost]));
return <span className={canAfford ? classes.money : classes.unbuyable}>{formatMoney(cost)}</span>;
}

View File

@@ -0,0 +1,11 @@
import React from "react";
import { PlayerObject } from "../../../PersonObjects/Player/PlayerObject";
import { Hashes } from "../../../ui/React/Hashes";
import { usePlayerSelector } from "../../../utils/PlayerExternalStore";
const selectHashCapacity = (p: PlayerObject) => p.hashManager.capacity;
export function PlayerHashCapacity(): React.ReactElement {
const capacity = usePlayerSelector(selectHashCapacity);
return <Hashes hashes={capacity} />;
}

View File

@@ -0,0 +1,11 @@
import React from "react";
import { PlayerObject } from "../../../PersonObjects/Player/PlayerObject";
import { Hashes } from "../../../ui/React/Hashes";
import { usePlayerSelector } from "../../../utils/PlayerExternalStore";
const selectHashes = (p: PlayerObject) => p.hashManager.hashes;
export function PlayerHashes(): React.ReactElement {
const hashes = usePlayerSelector(selectHashes);
return <Hashes hashes={hashes} />;
}

View File

@@ -0,0 +1,53 @@
import React, { useCallback } from "react";
import { Button, Tooltip } from "@mui/material";
import { HacknetServerConstants } from "../../data/Constants";
import { PlayerObject } from "../../../PersonObjects/Player/PlayerObject";
import { usePlayerSelector } from "../../../utils/PlayerExternalStore";
import { getMaxNumberLevelUpgrades, purchaseLevelUpgrade } from "../../HacknetHelpers";
import { Money } from "../../../ui/React/Money";
import { UpgradeHacknetServerLevelTooltip } from "./UpgradeHacknetServerLevelTooltip";
import { HacknetServer } from "../../HacknetServer";
interface IProps {
node: HacknetServer;
purchaseMult: number | string;
}
const selectHacknetNodeLevelCost = (p: PlayerObject) => p.mults.hacknet_node_level_cost;
const selectMoney = (p: PlayerObject) => p.money;
export function UpgradeHacknetServerLevelButton({ purchaseMult, node }: IProps): React.ReactElement {
const level = usePlayerSelector(useCallback((): number => node.level, [node]));
const hacknet_node_level_cost = usePlayerSelector(selectHacknetNodeLevelCost);
usePlayerSelector(selectMoney); // we have to hook on money change for the max level algorithm
function upgradeLevelOnClick(): void {
let numUpgrades = purchaseMult;
if (purchaseMult === "MAX") {
numUpgrades = getMaxNumberLevelUpgrades(node, HacknetServerConstants.MaxLevel);
}
purchaseLevelUpgrade(node, numUpgrades as number);
}
if (level >= HacknetServerConstants.MaxLevel) {
return <Button disabled>MAX LEVEL</Button>;
} else {
let multiplier = 0;
if (purchaseMult === "MAX") {
multiplier = getMaxNumberLevelUpgrades(node, HacknetServerConstants.MaxLevel);
} else {
const levelsToMax = HacknetServerConstants.MaxLevel - level;
multiplier = Math.min(levelsToMax, purchaseMult as number);
}
const upgradeLevelCost = node.calculateLevelUpgradeCost(multiplier, hacknet_node_level_cost) ?? 0;
return (
<Tooltip title={<UpgradeHacknetServerLevelTooltip purchaseMult={purchaseMult} node={node} />}>
<Button onClick={upgradeLevelOnClick}>
+{multiplier}&nbsp;-&nbsp;
<Money money={upgradeLevelCost} forPurchase={true} />
</Button>
</Tooltip>
);
}
}

View File

@@ -0,0 +1,48 @@
import { Typography } from "@mui/material";
import React, { useCallback } from "react";
import { calculateHashGainRate } from "../../formulas/HacknetServers";
import { PlayerObject } from "../../../PersonObjects/Player/PlayerObject";
import { HashRate } from "../../../ui/React/HashRate";
import { arrayShallowEquals, usePlayerSelector } from "../../../utils/PlayerExternalStore";
import { getMaxNumberLevelUpgrades } from "../../HacknetHelpers";
import { HacknetServerConstants } from "../../data/Constants";
import { HacknetServer } from "../../HacknetServer";
interface IProps {
node: HacknetServer;
purchaseMult: number | string;
}
const selectHacknetNodeMoney = (p: PlayerObject) => p.mults.hacknet_node_money;
export function UpgradeHacknetServerLevelTooltip({ purchaseMult, node }: IProps): React.ReactElement {
const [level, ramUsed, maxRam, cores] = usePlayerSelector(
useCallback((): [number, number, number, number] => [node.level, node.ramUsed, node.maxRam, node.cores], [node]),
arrayShallowEquals,
);
const hacknetNodeMoney = usePlayerSelector(selectHacknetNodeMoney);
let multiplier = 0;
if (purchaseMult === "MAX") {
multiplier = getMaxNumberLevelUpgrades(node, HacknetServerConstants.MaxLevel);
} else {
const levelsToMax = HacknetServerConstants.MaxLevel - level;
multiplier = Math.min(levelsToMax, purchaseMult as number);
}
const base_increase =
calculateHashGainRate(level + multiplier, 0, maxRam, cores, hacknetNodeMoney) -
calculateHashGainRate(level, 0, maxRam, cores, hacknetNodeMoney);
const modded_increase = (base_increase * (maxRam - ramUsed)) / maxRam;
return (
<Typography>
+<HashRate hashes={modded_increase} /> (effective increase, taking current RAM usage into account)
<br />
<span style={{ opacity: 0.5 }}>
+<HashRate hashes={base_increase} />
</span>{" "}
(base increase, attained when no script is running)
</Typography>
);
}

View File

@@ -35,13 +35,11 @@ import { calculateMoneyGainRate } from "../formulas/HacknetNodes";
interface IProps {
node: HacknetNode;
purchaseMultiplier: number | "MAX";
rerender: () => void;
}
export function HacknetNodeElem(props: IProps): React.ReactElement {
const node = props.node;
const purchaseMult = props.purchaseMultiplier;
const rerender = props.rerender;
// Upgrade Level Button
let upgradeLevelButton;
@@ -79,7 +77,6 @@ export function HacknetNodeElem(props: IProps): React.ReactElement {
const numUpgrades =
purchaseMult === "MAX" ? getMaxNumberLevelUpgrades(node, HacknetNodeConstants.MaxLevel) : purchaseMult;
purchaseLevelUpgrade(node, numUpgrades);
rerender();
}
let upgradeRAMButton;
@@ -121,14 +118,12 @@ export function HacknetNodeElem(props: IProps): React.ReactElement {
const numUpgrades =
purchaseMult === "MAX" ? getMaxNumberRamUpgrades(node, HacknetNodeConstants.MaxRam) : purchaseMult;
purchaseRamUpgrade(node, numUpgrades);
rerender();
}
function upgradeCoresOnClick(): void {
const numUpgrades =
purchaseMult === "MAX" ? getMaxNumberCoreUpgrades(node, HacknetNodeConstants.MaxCores) : purchaseMult;
purchaseCoreUpgrade(node, numUpgrades);
rerender();
}
let upgradeCoresButton;
if (node.cores >= HacknetNodeConstants.MaxCores) {

View File

@@ -25,16 +25,19 @@ import Typography from "@mui/material/Typography";
import Grid from "@mui/material/Grid";
import Button from "@mui/material/Button";
import { Box } from "@mui/material";
import { useCycleRerender } from "../../ui/React/hooks";
import { usePlayerSelector } from "../../utils/PlayerExternalStore";
import { PlayerObject } from "src/PersonObjects/Player/PlayerObject";
const selectHacknetNodecount = (p: PlayerObject) => p.hacknetNodes.length;
/** Root React Component for the Hacknet Node UI */
export function HacknetRoot(): React.ReactElement {
const [open, setOpen] = useState(false);
const rerender = useCycleRerender();
const [purchaseMultiplier, setPurchaseMultiplier] = useState<number | "MAX">(PurchaseMultipliers.x1);
const hacknetNodeCount = usePlayerSelector(selectHacknetNodecount);
let totalProduction = 0;
for (let i = 0; i < Player.hacknetNodes.length; ++i) {
for (let i = 0; i < hacknetNodeCount; ++i) {
const node = Player.hacknetNodes[i];
if (hasHacknetServers()) {
if (node instanceof HacknetNode) throw new Error("node was hacknet node"); // should never happen
@@ -53,7 +56,6 @@ export function HacknetRoot(): React.ReactElement {
function handlePurchaseButtonClick(): void {
purchaseHacknet();
rerender();
}
// Cost to purchase a new Hacknet Node
@@ -73,7 +75,7 @@ export function HacknetRoot(): React.ReactElement {
];
// HacknetNode components
const nodes = Player.hacknetNodes.map((node: string | HacknetNode) => {
const nodes = Player.hacknetNodes.map((node: string | HacknetNode, index: number) => {
if (hasHacknetServers()) {
if (node instanceof HacknetNode) throw new Error("node was hacknet node"); // should never happen
const hserver = GetServer(node);
@@ -84,16 +86,14 @@ export function HacknetRoot(): React.ReactElement {
return (
<HacknetServerElem
key={hserver.hostname}
index={index}
node={hserver}
purchaseMultiplier={purchaseMultiplier}
rerender={rerender}
/>
);
} else {
if (typeof node === "string") throw new Error("node was ip string"); // should never happen
return (
<HacknetNodeElem key={node.name} node={node} purchaseMultiplier={purchaseMultiplier} rerender={rerender} />
);
return <HacknetNodeElem key={node.name} node={node} purchaseMultiplier={purchaseMultiplier} />;
}
});

View File

@@ -6,11 +6,9 @@ import React from "react";
import { HacknetServerConstants } from "../data/Constants";
import {
getMaxNumberLevelUpgrades,
getMaxNumberRamUpgrades,
getMaxNumberCoreUpgrades,
getMaxNumberCacheUpgrades,
purchaseLevelUpgrade,
purchaseRamUpgrade,
purchaseCoreUpgrade,
purchaseCacheUpgrade,
@@ -34,65 +32,19 @@ import TableRow from "@mui/material/TableRow";
import { formatRam } from "../../ui/formatNumber";
import { calculateHashGainRate } from "../formulas/HacknetServers";
import Tooltip from "@mui/material/Tooltip";
import { HacknetServerLevel } from "./Components/HacknetServerLevel";
import { UpgradeHacknetServerLevelButton } from "./Components/UpgradeHacknetServerLevelButton";
interface IProps {
index: number;
node: HacknetServer;
purchaseMultiplier: number | string;
rerender: () => void;
}
export function HacknetServerElem(props: IProps): React.ReactElement {
const node = props.node;
const purchaseMult = props.purchaseMultiplier;
const rerender = props.rerender;
// Upgrade Level Button
let upgradeLevelButton;
if (node.level >= HacknetServerConstants.MaxLevel) {
upgradeLevelButton = <Button disabled>MAX LEVEL</Button>;
} else {
let multiplier = 0;
if (purchaseMult === "MAX") {
multiplier = getMaxNumberLevelUpgrades(node, HacknetServerConstants.MaxLevel);
} else {
const levelsToMax = HacknetServerConstants.MaxLevel - node.level;
multiplier = Math.min(levelsToMax, purchaseMult as number);
}
const base_increase =
calculateHashGainRate(node.level + multiplier, 0, node.maxRam, node.cores, Player.mults.hacknet_node_money) -
calculateHashGainRate(node.level, 0, node.maxRam, node.cores, Player.mults.hacknet_node_money);
const modded_increase = (base_increase * (node.maxRam - node.ramUsed)) / node.maxRam;
const upgradeLevelCost = node.calculateLevelUpgradeCost(multiplier, Player.mults.hacknet_node_level_cost);
upgradeLevelButton = (
<Tooltip
title={
<Typography>
+<HashRate hashes={modded_increase} /> (effective increase, taking current RAM usage into account)
<br />
<span style={{ opacity: 0.5 }}>
+<HashRate hashes={base_increase} />
</span>{" "}
(base increase, attained when no script is running)
</Typography>
}
>
<Button onClick={upgradeLevelOnClick}>
+{multiplier}&nbsp;-&nbsp;
<Money money={upgradeLevelCost} forPurchase={true} />
</Button>
</Tooltip>
);
}
function upgradeLevelOnClick(): void {
let numUpgrades = purchaseMult;
if (purchaseMult === "MAX") {
numUpgrades = getMaxNumberLevelUpgrades(node, HacknetServerConstants.MaxLevel);
}
purchaseLevelUpgrade(node, numUpgrades as number);
rerender();
}
const index = props.index;
function upgradeRamOnClick(): void {
let numUpgrades = purchaseMult;
@@ -100,7 +52,6 @@ export function HacknetServerElem(props: IProps): React.ReactElement {
numUpgrades = getMaxNumberRamUpgrades(node, HacknetServerConstants.MaxRam);
}
purchaseRamUpgrade(node, numUpgrades as number);
rerender();
}
// Upgrade RAM Button
let upgradeRamButton;
@@ -161,7 +112,6 @@ export function HacknetServerElem(props: IProps): React.ReactElement {
numUpgrades = getMaxNumberCoreUpgrades(node, HacknetServerConstants.MaxCores);
}
purchaseCoreUpgrade(node, numUpgrades as number);
rerender();
}
// Upgrade Cores Button
let upgradeCoresButton;
@@ -239,7 +189,6 @@ export function HacknetServerElem(props: IProps): React.ReactElement {
numUpgrades = getMaxNumberCacheUpgrades(node, HacknetServerConstants.MaxCache);
}
purchaseCacheUpgrade(node, numUpgrades as number);
rerender();
updateHashManagerCapacity();
}
@@ -299,9 +248,13 @@ export function HacknetServerElem(props: IProps): React.ReactElement {
<Typography>Level:</Typography>
</TableCell>
<TableCell>
<Typography>{node.level}</Typography>
<Typography>
<HacknetServerLevel index={index} />
</Typography>
</TableCell>
<TableCell>
<UpgradeHacknetServerLevelButton purchaseMult={purchaseMult} node={node} />
</TableCell>
<TableCell>{upgradeLevelButton}</TableCell>
</TableRow>
<TableRow>
<TableCell>

View File

@@ -7,14 +7,15 @@
import React from "react";
import { hasHacknetServers } from "../HacknetHelpers";
import { Player } from "@player";
import { Money } from "../../ui/React/Money";
import { MoneyRate } from "../../ui/React/MoneyRate";
import { HashRate } from "../../ui/React/HashRate";
import { Hashes } from "../../ui/React/Hashes";
import { Paper, Typography } from "@mui/material";
import { StatsTable } from "../../ui/React/StatsTable";
import { Tooltip } from "@mui/material";
import { HashTotalProduction } from "./Components/HashTotalProduction";
import { HacknetExpenses } from "./Components/HacknetExpenses";
import { HacknetProduced } from "./Components/HacknetProduced";
import { PlayerHashes } from "./Components/PlayerHashes";
import { PlayerHashCapacity } from "./Components/PlayerHashCapacity";
interface IProps {
totalProduction: number;
@@ -24,13 +25,13 @@ export function PlayerInfo(props: IProps): React.ReactElement {
const hasServers = hasHacknetServers();
const rows: React.ReactNode[][] = [];
rows.push(["Money Spent:", <Money key="money" money={-Player.moneySourceA.hacknet_expenses || 0} />]);
rows.push(["Money Produced:", <Money key="money" money={Player.moneySourceA.hacknet} />]);
rows.push(["Money Spent:", <HacknetExpenses key="expenses" />]);
rows.push(["Money Produced:", <HacknetProduced key="money" />]);
if (hasServers) {
rows.push([
"Hashes:",
<span key={"hashes"}>
<Hashes hashes={Player.hashManager.hashes} /> / <Hashes hashes={Player.hashManager.capacity} />
<PlayerHashes /> / <PlayerHashCapacity />
</span>,
]);
rows.push([
@@ -44,7 +45,7 @@ export function PlayerInfo(props: IProps): React.ReactElement {
}
>
<span>
<HashRate key="hashRate" hashes={props.totalProduction} />
<HashTotalProduction />
</span>
</Tooltip>,
]);

View File

@@ -1,10 +1,8 @@
import React from "react";
import { hasHacknetServers, hasMaxNumberHacknetServers } from "../HacknetHelpers";
import { Player } from "@player";
import { Money } from "../../ui/React/Money";
import Button from "@mui/material/Button";
import { MoneyCost } from "./Components/MoneyCost";
import { MoneyButton } from "./Components/MoneyButton";
interface IProps {
multiplier: number | string;
@@ -23,7 +21,7 @@ export function PurchaseButton(props: IProps): React.ReactElement {
text = (
<>
Purchase Hacknet Server -&nbsp;
<Money money={cost} forPurchase={true} />
<MoneyCost cost={cost} />
</>
);
}
@@ -31,14 +29,14 @@ export function PurchaseButton(props: IProps): React.ReactElement {
text = (
<>
Purchase Hacknet Node -&nbsp;
<Money money={cost} forPurchase={true} />
<MoneyCost cost={cost} />
</>
);
}
return (
<Button disabled={!Player.canAfford(cost)} onClick={props.onClick}>
<MoneyButton cost={cost} onClick={props.onClick}>
{text}
</Button>
</MoneyButton>
);
}

19
src/Hacknet/ui/utils.ts Normal file
View File

@@ -0,0 +1,19 @@
import { PlayerObject } from "src/PersonObjects/Player/PlayerObject";
import { HacknetServer } from "../HacknetServer";
import { HacknetNode } from "../HacknetNode";
import { GetServer } from "../../Server/AllServers";
export function safeGetHacknetServer(p: PlayerObject, index: number): HacknetServer | undefined {
const node = p.hacknetNodes[index];
if (node instanceof HacknetNode) return undefined;
const hserver = GetServer(node);
if (hserver == null) return undefined;
if (!(hserver instanceof HacknetServer)) return undefined;
return hserver;
}
export function safeGetHacknetNode(p: PlayerObject, index: number): HacknetNode | undefined {
const node = p.hacknetNodes[index];
if (!(node instanceof HacknetNode)) return undefined;
return node;
}

View File

@@ -49,6 +49,7 @@ import { EventEmitter } from "./utils/EventEmitter";
import { Companies } from "./Company/Companies";
import { resetGoPromises } from "./Go/boardAnalysis/goAI";
import { getRecordEntries } from "./Types/Record";
import { runSelectors } from "./utils/PlayerExternalStore";
declare global {
// This property is only available in the dev build
@@ -421,6 +422,7 @@ const Engine = {
if (GameCycleEvents.hasSubscribers()) {
ReactDOM.unstable_batchedUpdates(() => {
GameCycleEvents.emit();
runSelectors();
});
}
}

View File

@@ -0,0 +1,52 @@
import { Player } from "@player";
import { useEffect, useRef, useState } from "react";
import { PlayerObject } from "src/PersonObjects/Player/PlayerObject";
const trieq = <T>(a: T, b: T): boolean => a === b;
export const arrayShallowEquals = <T>(a: T[], b: T[]): boolean => {
if (a.length != b.length) return false;
return a.every((v, i) => b[i] === v);
};
export const runSelectors = (): void => {
for (const sub of subscriptions) sub(Player);
};
const subscriptions: ((g: PlayerObject) => void)[] = [];
const subscribe = (check: (g: PlayerObject) => void): void => {
subscriptions.push(check);
};
const unsubscribe = (check: (g: PlayerObject) => void) => {
return (): void => {
let found = -1;
for (let i = 0; i < subscriptions.length; i++) {
if (subscriptions[i] !== check) continue;
found = i;
break;
}
if (found === -1) return;
subscriptions[found] = subscriptions[subscriptions.length - 1];
subscriptions.length -= 1;
};
};
const incr = (n: number): number => n + 1;
export const usePlayerSelector = <T>(f: (g: PlayerObject) => T, eq: (a: T, b: T) => boolean = trieq): T => {
const [, setValue] = useState(0);
const ref = useRef(f(Player));
useEffect(() => {
const check = (g: PlayerObject): void => {
const next = f(g);
if (eq(ref.current, next)) return;
ref.current = next;
setValue(incr);
};
subscribe(check);
return unsubscribe(check);
}, [f, eq]);
return ref.current;
};