Skip to content

Commit

Permalink
Create useMemoizedCallback
Browse files Browse the repository at this point in the history
  • Loading branch information
littensy committed Apr 22, 2023
1 parent 9eceede commit 57b2abc
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 5 deletions.
11 changes: 6 additions & 5 deletions src/use-binding-effect/use-binding-effect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Binding } from "@rbxts/roact";
import { useEffect, useMemo, useMutable } from "@rbxts/roact-hooked";
import { useEffect, useMemo } from "@rbxts/roact-hooked";
import { useMemoizedCallback } from "../use-memoized-callback";
import { getBindingApi, isBinding } from "../utils/binding";

/**
Expand All @@ -18,14 +19,14 @@ export function useBindingEffect<T>(binding: T | Binding<T>, effect: (value: T)
return isBinding<T>(binding) ? getBindingApi(binding) : undefined;
}, [binding]);

const effectRef = useMutable(effect);
const effectCallback = useMemoizedCallback(effect);

useEffect(() => {
if (api) {
effectRef.current(api.getValue());
return api.subscribe((value) => effectRef.current(value));
effectCallback(api.getValue());
return api.subscribe(effectCallback);
} else {
effectRef.current(binding as T);
effectCallback(binding as T);
}
}, [binding]);
}
37 changes: 37 additions & 0 deletions src/use-memoized-callback/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## 🪝 `useMemoizedCallback`

```ts
function useMemoizedCallback<T extends Callback>(callback: T): T;
```

Returns a memoized callback. When passed a new callback, the memoized callback will not change, but calling it will invoke the new callback.

### 📕 Parameters

- `callback` - The callback to memoize.

### 📗 Returns

- The memoized callback.

### 📘 Example

```tsx
interface Props {
onStep: () => void;
}

export default function Component({ onStep }: Props) {
const onStepCallback = useMomoizedCallback(onStep);

useEffect(() => {
const connection = RunService.Heartbeat.Connect(onStepCallback);

return () => {
connection.Disconnect();
};
}, [onStepCallback]);

return <frame />;
}
```
1 change: 1 addition & 0 deletions src/use-memoized-callback/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./use-memoized-callback";
43 changes: 43 additions & 0 deletions src/use-memoized-callback/use-memoized-callback.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/// <reference types="@rbxts/testez/globals" />

import { renderHook } from "../utils/testez";
import { useMemoizedCallback } from "./use-memoized-callback";

export = () => {
it("should memoize the callback on mount", () => {
const callback = () => {};
const { result } = renderHook(() => useMemoizedCallback(callback));
expect(result.current).to.be.a("function");
expect(result.current).never.to.equal(callback);
});

it("should return memoized callback after unrelated rerender", () => {
const { result, rerender } = renderHook(() => useMemoizedCallback(() => {}));
const memoizedCallback = result.current;
rerender();
expect(result.current).to.equal(memoizedCallback);
});

it("should return memoized callback after passed callback changes", () => {
const { result, rerender } = renderHook(({ callback }) => useMemoizedCallback(callback), {
initialProps: { callback: () => {} },
});
const memoizedCallback = result.current;
rerender({ callback: () => {} });
expect(result.current).to.equal(memoizedCallback);
rerender({ callback: () => {} });
expect(result.current).to.equal(memoizedCallback);
});

it("should memoize the passed callback", () => {
const { result, rerender } = renderHook(({ callback }) => useMemoizedCallback(callback), {
initialProps: {
callback: (a: number, b: number) => a + b,
},
});
const memoizedCallback = result.current;
expect(memoizedCallback(1, 2)).to.equal(3);
rerender({ callback: (a: number, b: number) => a - b });
expect(memoizedCallback(1, 2)).to.equal(-1);
});
};
11 changes: 11 additions & 0 deletions src/use-memoized-callback/use-memoized-callback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useCallback, useMutable } from "@rbxts/roact-hooked";

export function useMemoizedCallback<T extends Callback>(callback: T): T;
export function useMemoizedCallback(callback: Callback): Callback {
const callbackRef = useMutable(callback);
callbackRef.current = callback;

return useCallback((...args) => {
return callbackRef.current(...args);
}, []);
}
14 changes: 14 additions & 0 deletions src/utils/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ export function isBinding(value: unknown): value is Binding<unknown> {
return typeIs(value, "table") && "getValue" in value && "map" in value;
}

/**
* Returns the value of a binding. If the given value is not a binding, it will
* be returned as-is.
* @param binding The binding to get the value of.
* @returns The value of the binding.
*/
export function getBindingValue<T>(binding: T | Binding<T>): T {
if (isBinding(binding)) {
return binding.getValue();
} else {
return binding;
}
}

/**
* Maps a binding to a new binding. If the given value is not a binding, it will
* be passed to the mapper function and returned as a new binding.
Expand Down

0 comments on commit 57b2abc

Please sign in to comment.