Having to add additional dependencies to react useCallback dependency list - javascript

I'm trying to create a custom hook function that uses useCallback
function. Here's a toy example:
export function useCustomLink(link: { url: string; description: string }) {
const [updating, setUpdating] = useState<boolean>(false);
const [linkInfo, setLinkInfo] = useState<{ url: string; description: string } | "loading" | "error">("loading");
useEffect(() => {
// initiate linkInfo
if (!updating) {
setLinkInfo(link);
}
}, [link]);
const updateLink = useCallback((update: Partial<{ url: string; description: string }>) => {
if (linkInfo !== 'loading' && linkInfo !== 'error') {
const updated = { ...linkInfo, ...update };
setLinkInfo(updated);
setUpdating(true);
}
}, [linkInfo, link]);
return {
linkInfo, updateLink
};
}
My understanding of the dependency list is that I only need to add variables that are directly used by the function (in this case, linkInfo). However, if I don't add link (which is passed into the custom hook function) into the list as well, then I would end up with stale states of linkInfo. Can anyone help explain why I need to add the add'l variable into the dependencies? Is this the correct way to use useCallback?
Thanks!

Related

Giving Typescript classes access to React hooks state

I'm trying to make a game using React to display the UI elements and using Typescript classes to represent the state of the game.
Here are a few examples of the classes I'm using to represent my data:
export class Place extends Entity {
items: Item[];
npcs: NPC[];
location: LatLng | null;
onEnter: (...args: any[]) => any = () => {};
constructor(
name: string,
description: string,
location?: LatLng,
onEnter: (...args: any[]) => any = () => {},
items: Item[] = [],
npcs: NPC[] = []
) {
super(name, description);
this.items = items;
this.npcs = npcs;
this.location = location ? location : null;
this.onEnter = onEnter;
}
export class Item extends Entity {
url: string;
constructor(
name: string,
description: string,
actions = {},
url = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Question_mark_%28black%29.svg/1920px-Question_mark_%28black%29.svg.png"
) {
super(name, description);
this.url = url;
this.actions = actions;
}
}
export class NPC {
name: string;
description: string;
messages: Message[];
url: string;
timesTalkedTo = 0;
constructor(
name: string,
description: string,
url = "https://cdn.icon-icons.com/icons2/1378/PNG/512/avatardefault_92824.png",
messages: Message[] = []
) {
this.name = name;
this.description = description;
this.messages = messages;
this.url = url;
}
getMsg() {
console.log(this.messages);
if (this.messages.length > 1) {
for (var i = 1; i < this.messages.length; i++) {
const msg = this.messages[i];
if (msg["cond"] && msg["cond"]()) {
this.timesTalkedTo += 1;
return msg;
}
}
}
this.timesTalkedTo += 1;
return this.messages[0];
}
}
Later on, I store instances of these classes in hooks so I can display them using other components I've defined:
function UI() {
const [places, setPlaces] = useState({});
const [inventory, setInventory] = useState([]);
const [playerPlace, setPlayerPlace] = useState(outside);
const [playerLocation, setPlayerLocation] = useState(L.latLng([0, 0]));
...
My problem is that I wanted to define a class and functions like this inside my UI component, so I would be able to access the setState hooks and use the "drop" and "pick up" actions on any item I've defined as Droppable:
class Droppable extends Item {
dropped;
constructor(
name,
description,
actions = {},
dropped = true,
url = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Question_mark_%28black%29.svg/1920px-Question_mark_%28black%29.svg.png"
) {
super(name, description, actions, url);
this.dropped = dropped;
const drop = () => {
addToPlace(removeFromInventory(this));
this.dropped = true;
this.actions["pick up"] = pickUp;
delete this.actions["drop"];
};
const pickUp = () => {
addToInventory(removeFromPlace(this));
this.dropped = false;
this.actions["drop"] = drop;
delete this.actions["pick up"];
};
if (dropped) {
this.actions["pick up"] = pickUp;
} else {
this.actions["drop"] = drop;
}
}
}
const addToInventory = useCallback(
(item) => {
setInventory((inv) => [...inv, item]);
return item;
},
[setInventory]
);
const removeFromInventory = useCallback(
(item) => {
setInventory((inv) => inv.filter((i) => i !== item));
return item;
},
[setInventory]
);
const addToPlace = useCallback(
(item) => {
setPlaces((places) => {
return {
...places,
[playerPlace.name]: {
...playerPlace,
items: [...playerPlace.items, item],
},
};
});
return item;
},
[setPlaces, playerPlace]
);
const removeFromPlace = useCallback(
(item) => {
setPlaces((places) => {
const newPlace = { ...places[playerPlace.name] };
newPlace.items = newPlace.items.filter((i) => i !== item);
const newPlaces = [...places];
newPlaces[playerPlace.name] = newPlace;
return newPlaces;
});
return item;
},
[setPlaces, playerPlace]
);
However, when I try removing an item from the place it's in and adding it to the player's inventory (the "pick up" action), I find that, while it is successfully added to the inventory, it cannot be removed from the place, because the playerPlace state variable is stale. Even though setPlayerPlace had been called successfully and set the playerPlace to a place containing items, the value is still set to its initial empty Place, so there is an error when trying to access the items of that Place.
My guess is that these callbacks are not being updated properly according to the state because they are used inside the class that I defined, but I can't think of any other way to give methods inside the class easy access to the state variables.
Is it a bad idea to be using ordinary classes alongside React in this way? If so, what would be a better way to structure my app. If not, how can I give my classes access to the state inside my React components?
I would recommend moving the class outside of the component and then passing the setters and data to the class as parameters if you really want to use classes. You can also use a third-party state management library for this and then hook it together, but I don't think it's really worth it. Generally speaking, using classes for your state in react is an antipattern IMHO. Usually what I would do is just write types and then utility functions for those types if I need to encapsulate functionality. This has many benefits aside from working with react such as easily being able to serialize the data to JSON (they are now POJOs).

Sending some but not all args to a function without defining nulls

I'm using vue 3 and composable files for sharing some functions through my whole app.
My usePluck.js composable file looks like
import { api } from 'boot/axios'
export default function usePlucks() {
const pluckUsers = ({val = null, excludeIds = null}) => api.get('/users/pluck', { params: { search: val, exclude_ids: excludeIds }})
return {
pluckUsers
}
}
In order to make use of this function in my component I do
<script>
import usePlucks from 'composables/usePlucks.js'
export default {
name: 'Test',
setup() {
const { pluckUsers } = usePlucks()
onBeforeMount(() => {
pluckUsers({excludeIds: [props.id]})
})
return {
}
}
}
</script>
So far so good, but now I'd like to even be able to not send any args to the function
onBeforeMount(() => {
pluckUsers()
})
But when I do that, I get
Uncaught TypeError: Cannot read properties of undefined (reading 'val')
I assume it's because I'm not sending an object as argument to the function, therefore I'm trying to read val from a null value: null.val
What I'm looking for is a way to send, none, only one, or all arguments to the function with no need to set null values:
// I don't want this
pluckUsers({})
// Nor this
pluckUsers({val: null, excludeIds: [props.id]})
I just want to send only needed args.
Any advice about any other approach will be appreciated.
import { api } from 'boot/axios'
export default function usePlucks() {
const pluckUsers = ({val = null, excludeIds = null} = {}) => api.get('/users/pluck', { params: { search: val, exclude_ids: excludeIds }})
return {
pluckUsers
}
}
I believe this is what you're looking for. The { ... } = {}
EDIT: It didn't work without this because with no argument the destructuring failed because it can't match an object. That's why you also need a default value on the parameter object, also called simulated named parameter.

How to write a constructor function in TypeScript that has method extension functionality

I want to write a constructor function that takes some predefined methods from an object (Methods) and injects it into every new object that is created with the constructor. It injects methods from another object because, I want the consumers of my module to be able to add new methods.
But the problem is: as all the injected methods are not defined in the constructor I can't seem to manage their type annotations properly.
It's hard for me to describe the problem so I created a simple example (with JavaScript to avoid all the type error) to demonstrate it.
// methods.js ---------------------------------------
const methods = {};
const addMethod = (name, value) => (methods[name] = value);
// these methods should be added by an external user of the programmer.js module
function code(...task) {
this.addInstruction({
cmd: "code",
args: task
});
return this;
}
function cry(...words) {
this.addInstruction({
cmd: "cry",
args: words
});
return this;
}
addMethod("code", code);
addMethod("cry", cry);
// programmer.js -------------------------------------
const retriveInstructionsMethod = "SECRET_METHOD_NAME";
const secretKey = "VERY_SECRET_KEY";
function Programmer() {
const instructions = [];
this.addInstruction = (value) => instructions.push(value);
Object.defineProperty(this, retriveInstructionsMethod, {
enumerable: false,
writable: false,
value(key) {
if (key === secretKey) return instructions;
},
});
for (const key of Object.keys(methods))
this[key] = (...args) => methods[key].apply(this, args);
}
// test.js -------------------------------------------
const desperateProgrammer = new Programmer();
const instructions = desperateProgrammer
.code("A library in typescript within 10 days")
.cry("Oh God! Why everything is so complicated :'( ? Plz Help!!!")
// the next two lines shouldn't work here (test.js) as the user
// shouln't have asscess to the "retriveInstructionsMethod" and "secretKey"
// keys. I'm just showing it to demonstrate what I want to achieve.
[retriveInstructionsMethod](secretKey);
console.log(instructions);
Here I want to hide all the instructions given to a Programmer object. If I hide it from the end user then I won't have to validate those instructions before executing them later.
And the user of programmer.js module should be able to add new methods to a programmer. For example:
// addMethods from "methods.js" module
addMethods("debug", (...bugs) => {...});
Now I know that, I can create a base class and just extend it every time I add a new method. But as it is expected that there will be lots of external methods so soon it will become very tedious for the user.
Below is what I've tried so far. But the type annotation clearly doesn't work with the following setup and I know it should not! Because the Methods interface's index signature([key: string]: Function) is very generic and I don't know all the method's name and signature that will be added later.
methods.ts
export interface Methods {
[key: string]: Function;
}
const methods: Methods = {};
export default function getMethods(): Methods {
return { ...methods };
}
export function addMethods<T extends Function>(methodName: string, method: T) {
methods[methodName] = method;
}
programmer.ts
import type { Methods } from "./methods";
import getMethods from "./methods";
export type I_Programmer = Methods & {
addInstruction: (arg: { cmd: string; args: unknown[] }) => void;
};
interface ProgrammerConstructor {
new (): I_Programmer;
(): void;
}
const retriveInstructionsMethod = "SECRET_METHOD_NAME";
const secretKey = "ACCESS_KEY";
const Programmer = function (this: I_Programmer) {
const instructions: object[] = [];
this.addInstruction = (value) => instructions.push(value);
Object.defineProperty(this, retriveInstructionsMethod, {
enumerable: false,
writable: false,
value(key: string) {
if (key === secretKey) return instructions;
},
});
const methods = getMethods();
for (const key of Object.keys(methods))
this[key] = (...args: unknown[]) => methods[key].apply(this, args);
} as ProgrammerConstructor;
// this function is just to demonstrate how I want to extract all the
// instructions. It should not be accessible to the end user.
export function getInstructionsFrom(programmer: I_Programmer): object[] {
// gives error
// #ts-expect-error
return programmer[retriveInstructionsMethod][secretKey]();
}
export default Programmer;
testUsages.ts
import { addMethods } from "./methods";
import type { I_Programmer } from "./programmer";
import Programmer, { getInstructionsFrom } from "./programmer";
function code(this: I_Programmer, task: string, deadline: string) {
this.addInstruction({ cmd: "code", args: [task, deadline] });
return this;
}
function cry(this: I_Programmer, words: string) {
this.addInstruction({ cmd: "cry", args: [words] });
return this;
}
addMethods("code", code);
addMethods("cry", cry);
const desperateProgrammer = new Programmer()
.code("a library with typescript", "10 days") // no type annotation of "code" method
.cry("Oh God! Why everything is so complicated :'( ? Plz Help!!!"); // same here
// Just for demonstration. Should not be accessible to the end user!!!
console.log(getInstructionsFrom(desperateProgrammer));
Kindly give me some hint how I can solve this problem. Thanks in advance.

Typescript Error in React useScroll hook: Cannot invoke an expression whose type lacks a call signature

I have the following function definition:
const useScroll = () => {
const ref = useRef(null)
function executeScroll() {
if (ref !== null)
window.scrollTo(0, ref.current.offsetTop)
}
const htmlElementAttributes = { ref }
return [executeScroll, htmlElementAttributes]
}
export default useScroll;
Based on this function, I have the following code:
const [executeScroll, scrollHtmlAttributes] = useScroll();
const click_argueBtn = (e) => {
e.preventDefault();
executeScroll();//error
}
However, the executeScroll(); code throws the following error:
Error: Cannot invoke an expression whose type lacks a call signature
Any ideas why I receive this error? My code is based on this post.
Typescript is doing its best to determine the types automatically, and it determines that useScroll returns an array, who's elements are each () => void | { ref: /*ref type*/ } (i don't know precisely what the type on the ref object is). When you try to call executeScroll, it doesn't know whether it's a function, or an object with a ref. So since it might not be a function, you're not allowed to call it.
Instead, i'd recommend explicitly telling typescript that useScroll returns a tuple. A tuple is similar to an array, except you explicitly declare how many elements it has and what their individual types are:
const useScroll = (): [() => void, { ref: /* ref type */ }] => {
// ...etc
}
or
const useScroll = () => {
// ... etc
return [executeScroll, htmlElementAttributes] as [() => void, { ref: /* ref type */ }];
}
or if you don't like it inline, you could extract it to a type:
export type UseScrollResult = [() => void, { ref: /* ref type */ }];
const useScroll = (): UseScrollResult => {
// ...etc
}
From this, typescript now knows that element 0 of the array is () => void, and therefore it's legal to call it as a function.

TypeScript - Send optional field on POST only when there is value

I am using Formik, in my React application. I have a simple form with 3 fields. I am doing 2 operations with that form. Add/Edit Resources.
My Problem is that one field is optional. Meaning I should never send it, if its value is null. Currently, I send an empty string which is wrong.
I am using TS-React-Formik, and here is my code for the handleSubmit method:
interface IValues extends FormikValues {
name: string;
owner?: string;
groups: string[];
}
interface CreateAndEditProps {
doSubmit(service: object, values: object): AxiosResponse<string>;
onSave(values: IValues): void;
}
handleSubmit = (values: FormikValues, formikActions:FormikActions<IValues>) => {
const { doSubmit, onSave, isEditMode } = this.props;
const { setSubmitting } = formikActions;
const payload: IValues = {
name: values.name,
groups: values.groups,
owner: values.owner
};
const submitAction = isEditMode ? update : create;
return doSubmit(submitAction, payload)
.then(() => {
setSubmitting(false);
onSave(payload);
})
.catch(() => {
setSubmitting(false);
});
};
I thought a simple if statement would work, and while it does, I do not like it at all. Let me give you an example of why. If I add 2 more optional fields, as I am about to do, in a similar form, I do not want to do several if statements to achieve that.
If you could think of a more elegant and DRY way of doing it, It would be amazing. Thank you for your time.
Look at the removeEmptyKeys() below.
It takes in an Object and removes the keys that have empty string.It mutates the original Object, please change it accordingly if you expect a diff behaviour.
In your code after defining payload, I would simply call this method , removeEmptyKeys(payload)
Also it will resolve your if else problem.
removeEmptyKeys = (item)=>{
Object.keys(item).map((key)=>{
if(payload[key]===""){
delete payload[key]}
})}
var payload = {
one : "one",
two : "",
three : "three"
}
removeEmptyKeys(payload)
Please mark it as resolved if you find this useful.
For your code :
const removeEmptyKeys = (values: IValues): any => {
Object.keys(values).map((key) => {
if (payload && payload[key] === "")
{ delete payload[key] } })
return values;
}

Categories

Resources