Why is hook not reusable? - reactjs

I created a custom hook as follows:
import { useState } from 'react';
export const useMyHook = () => {
const [isVisible, setIsVisible] = useState(false);
function toggle() {
setIsVisible(!isVisible);
}
return { isVisible, toggle,}
};
I am only able to use it once (see comment below). When I call the hook again with different const, I get the error:
Property 'isVisible2' does not exist on type '{ isVisible: boolean; toggle: () => void; }'. TS2339
import React from 'react';
import useModal from './useMyHook';
export const App = () => {
const {isVisible, toggle} = useMyHook(); // Example of using once
const {isVisible2, toggle2} = useMyHook(); // am not able to use it here
const {isVisible3, toggle3} = useMyHook(); // am not able to use it here
return (<div> Hello world! </div>);
};
I am incorrectly assuming that creating a new const var allows the reuse of the hook. What can I do to fix this?

Right now, you're returning an object with two properties: isVisible and toggle.
Either destructure into differently-named variables (verbose; not great):
const {isVisible: isVisible2, toggle: toggle2} = useMyHook();
Or return an array from the hook instead:
return [isVisible, toggle];
and
const [isVisible2, toggle2] = useMyHook();

Because in your custom hook, you return an object with 2 properties: isVisible and toggle.
But you destructured it into isVisible2 and toggle2. You can see, there are no isVisible2 and toggle2 in your return.
So, if you want to use this hook again try to assign it to a new variable name like this:
const {isVisible: isVisible2, toggle: toggle2} = useMyHook();

Related

Writing a TypeScript Interface for React Context, how to describe the interface/ different types to be passed as values?

I am trying to write a React Context for my application
This is a follow up from my previous question:
How to pass value to Context Provider in TypeScript?
I would please like some help describing different value types to be passed through the provider.
Overview:
I am currently trying to construct a context provider that can be used across the scope of my application in TypeScript.
It contains some useState hooks and and an asynchronous function to be passed through as values the provider to all the child components.
ResultConextProvider.tsx
export const ResultContextProvider = () => {
const [isLoading, setIsLoading] = useState<boolean>(false)
const [greenStatus, setGreenStatus] =
useState(new Set<MyEnum>());
const [redStatus, setRedStatus] =
useState(new Set<MyEnum>());
const [searchTerm, setSearchTerm] = useState<string>('')
// objects to be passed values
const greenValue = {greenStatus, setGreenStatus};
const redValue = {redStatus, setRedStatus};
const searchValue = {searchTerm, setSearchTerm};
// api function coming from tested API spec (external)
const getResults = async () => {
setIsLoading(true)
myAPI.myGet(greenStatus, redStatus).then((result) => {
setResults(result.data);
})
setIsLoading(false)
}
return (
<ResultContext.Provider value={{getResults, greenValue, redValue, searchValue}}>
{children}
</ResultContext.Provider>
}
export const useResultContext = () => useContext(ResultContext);
As you can see above, I would like to pass the getResults function, my greenValues, redValus and searchValues to all my child components, the Provider implentation will look something like this:
index.tsx
import { ResultContextProvider } from "./contexts/ResultContextProvider";
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<ResultContextProvider
<Router>
<App />
</Router>
</ResultContextProvider>
);
When it comes to writing the interface, I am struggling to understand what needs to be represented prior to my component.
So far my interface looks something like this:
ResultConextProvider.tsx
interface ContextParametersType {
greenValue: { greenStatus: Set<MyEnum>, setGreenStatus:
Dispatch<SetStateAction<Set<MyEnum>>> };
redValue: { redStatus: Set<MyEnum>, setRedStatus:
Dispatch<SetStateAction<Set<MyEnum>>> };
///INCORRECT //
// searchValue: {searchTerm: <string>(''), setSearchTerm:
Dispatch<SetStateAction<string> };
///UNSURE
// getResults : {
// What to write as interface for getResults
// }
}
I have worked out what needs to be declared for my greenValue and redValue, based on the answer to my previous question however struggling to fill out the rest of the interface specification
The Search Term:
///INCORRECT //
// searchValue: {searchTerm: (''), setSearchTerm:
Dispatch<SetStateAction };
I have tried to follow the pattern of the Enum state, particularly in declaring the state as type string and rerferecning the Dispatch<SetStateAction as the same type to change the state however this produces error
getResults
This is an asynchronous function with an Axios function inside, to be truthful i do not know where to begin within this one.
My confusion on these two steps persists when creating the ResultContext
const ResultContext = createContext<ContextParametersType>({
greenValue: {
greenStatus: new Set<FirmStatusEnum>(), setGreenStatus: () => {
}
},
redValue: {
redStatus: new Set<FirmStatusEnum>(), setRedStatus: () => {
}
},
// searchValue: {
// searchTerm: <string>(''), setSearchTerm: () => {
// }
// },
getResults()
// Not Sure how to initalsie this?
});
Does this make sense? Am I understanding the issues here correctly?
I would really like some help in understanding how to configure the context interfaces for this.
It is essentially a typescript adaptation of this tutorial
Here:
Thanks, please let me know if I need to provide clarity

Typescript complaining when calling a method from React useState initialized with an empty object

In the main App component I have an event handler that passes an onSubmit and onClose callback to the onOpenDialog function
App.ts
const App = () => {
const { onOpenDialog, onCloseDialog } = useDialog()
const onOpenModal = () => {
// Open the Dialog and pass callbacks
onOpenDialog({
onSubmit: (data: any) => {
// ... CRUD operation
onCloseDialog()
},
onCancel: onCloseDialog
})
return <button onClick={onOpenModal}>Open</button>
}
The onOpenDialog function comes from the useDialog hook which stores the callback in the Dialog state.
This state is initialized with an empty object ({}). It is populated when the modal is open and reseted to its initial value ({}) when the modal is closed.
useDialog.ts
const useDialog = () => {
const [open, setOpen] = useState<boolean>(false)
const [dialogState, setDialogState] = useState<object>({})
const onOpenDialog = (config?: object): void => {
setOpen(true)
if (isObject(config) && !isEmpty(config)) {
setDialogState(config!)
}
}
const onCloseDialog = (): void => {
setOpen(false)
if (!isEmpty(dialogState)) {
setDialogState({})
}
}
}
Finally, I have a hook for my modal form component that exports two handlers (onSubmit and onCancel) which will be called in the modal form component
useModalForm.ts
const useModalForm = () => {
const { dialogState } = useDialog()
const onSubmit = (data: any) => {
dialogState.onSubmit(data) // Property 'onSubmit' does not exist on type 'object'
}
return {
onSubmit: onSubmit,
onCancel: dialogState.onCancel,
}
}
I can't call dialogState.onSubmit(data) because typescript complains that onSubmit doesn't exist in type 'object' (which I understand)
How can I - in Typescript - set the initial/closed state to {} and populate it with whatever callback or data I pass when the modal is open.
I tried to type useState with an interface of the signature of the expected state but when I close the modal, I can't reset the dialogConfig to {} without Typescript complaining.
I had the same problem and tried the same solutions.
Here's what I found in the React+TypeScript Cheatsheets:
Approach 1
Create a type for the expected state then "explicitly declare the type, and use a union type".
type IDialogState = { /* Describe the shape of your state */ };
const [dialogState, setDialogState] = React.useState<IDialogState | null>(null);
Approach 2
"You can also use type assertions if a state is initialized soon after setup and always has a value after:"
type IDialogState = { /* Describe the shape of your state */ };
const [dialogState, setDialogState] = React.useState<IDialogState>({} as IDialogState);
"This temporarily 'lies' to the TypeScript compiler that {} is of type IDialogState. You should follow up by setting the user state — if you don't, the rest of your code may rely on the fact that user is of type IDialogState and that may lead to runtime errors."
Source: React+TypeScript Cheatsheets
(variable names adapted)
Related:
as in React Hooks to give the initial value an empty object
https://jsramblings.com/how-to-use-usestate-hook-in-react-with-typescript/
https://www.carlrippon.com/typed-usestate-with-typescript/

How to use useEffect correctly with useContext as a dependency

I'm working on my first React project and I have the following problem.
How I want my code to work:
I add Items into an array accessible by context (context.items)
I want to run a useEffect function in a component, where the context.items are displayed, whenever the value changes
What I tried:
Listing the context (both context and context.items) as a dependency in the useEffect
this resulted in the component not updating when the values changed
Listing the context.items.length
this resulted in the component updating when the length of the array changed however, not when the values of individual items changed.
wraping the context in Object.values(context)
result was exactly what I wanted, except React is now Complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
Do you know any way to fix this React warning or a different way of running useEffect on context value changing?
Well, didn't want to add code hoping it would be some simple error on my side, but even with some answers I still wasn't able to fix this, so here it is, reduced in hope of simplifying.
Context component:
const NewOrder = createContext({
orderItems: [{
itemId: "",
name: "",
amount: 0,
more:[""]
}],
addOrderItem: (newOItem: OrderItem) => {},
removeOrderItem: (oItemId: string) => {},
removeAllOrderItems: () => {},
});
export const NewOrderProvider: React.FC = (props) => {
// state
const [orderList, setOrderList] = useState<OrderItem[]>([]);
const context = {
orderItems: orderList,
addOrderItem: addOItemHandler,
removeOrderItem: removeOItemHandler,
removeAllOrderItems: removeAllOItemsHandler,
};
// handlers
function addOItemHandler(newOItem: OrderItem) {
setOrderList((prevOrderList: OrderItem[]) => {
prevOrderList.unshift(newOItem);
return prevOrderList;
});
}
function removeOItemHandler(oItemId: string) {
setOrderList((prevOrderList: OrderItem[]) => {
const itemToDeleteIndex = prevOrderList.findIndex((item: OrderItem) => item.itemId === oItemId);
console.log(itemToDeleteIndex);
prevOrderList.splice(itemToDeleteIndex, 1);
return prevOrderList;
});
}
function removeAllOItemsHandler() {
setOrderList([]);
}
return <NewOrder.Provider value={context}>{props.children}</NewOrder.Provider>;
};
export default NewOrder;
the component (a modal actually) displaying the data:
const OrderMenu: React.FC<{ isOpen: boolean; hideModal: Function }> = (
props
) => {
const NewOrderContext = useContext(NewOrder);
useEffect(() => {
if (NewOrderContext.orderItems.length > 0) {
const oItems: JSX.Element[] = [];
NewOrderContext.orderItems.forEach((item) => {
const fullItem = {
itemId:item.itemId,
name: item.name,
amount: item.amount,
more: item.more,
};
oItems.push(
<OItem item={fullItem} editItem={() => editItem(item.itemId)} key={item.itemId} />
);
});
setContent(<div>{oItems}</div>);
} else {
exit();
}
}, [NewOrderContext.orderItems.length, props.isOpen]);
some comments to the code:
it's actually done in Type Script, that involves some extra syntax
-content (and set Content)is a state which is then part of return value so some parts can be set dynamically
-exit is a function closing the modal, also why props.is Open is included
with this .length extension the modal displays changes when i remove an item from the list, however, not when I modify it not changeing the length of the orderItems,but only values of one of the objects inside of it.
as i mentioned before, i found some answers where they say i should set the dependency like this: ...Object.values(<contextVariable>) which technically works, but results in react complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
the values displayed change to correct values when i close and reopen the modal, changing props.isOpen indicating that the problem lies in the context dependency
You can start by creating your app context as below, I will be using an example of a shopping cart
import * as React from "react"
const AppContext = React.createContext({
cart:[]
});
const AppContextProvider = (props) => {
const [cart,setCart] = React.useState([])
const addCartItem = (newItem)=>{
let updatedCart = [...cart];
updatedCart.push(newItem)
setCart(updatedCart)
}
return <AppContext.Provider value={{
cart
}}>{props.children}</AppContext.Provider>;
};
const useAppContext = () => React.useContext(AppContext);
export { AppContextProvider, useAppContext };
Then you consume the app context anywhere in the app as below, whenever the length of the cart changes you be notified in the shopping cart
import * as React from "react";
import { useAppContext } from "../../context/app,context";
const ShoppingCart: React.FC = () => {
const appContext = useAppContext();
React.useEffect(() => {
console.log(appContext.cart.length);
}, [appContext.cart]);
return <div>{appContext.cart.length}</div>;
};
export default ShoppingCart;
You can try passing the context variable to useEffect dependency array and inside useEffect body perform a check to see if the value is not null for example.

Test useRef onError Fn, with React-Testing-Library and Jest

I have this simple fallbackImage Component:
export interface ImageProps {
srcImage: string;
classNames?: string;
fallbackImage?: FallbackImages;
}
const Image = ({
srcImage,
classNames,
fallbackImage = FallbackImages.FALLBACK
}: ImageProps) => {
const imgToSourceFrom = srcImage;
const imgToFallbackTo = fallbackImage;
const imageRef = useRef(null);
const whenImageIsMissing = () => {
imageRef.current.src = imgToFallbackTo;
imageRef.current.onerror = () => {};
};
return (
<img ref={imageRef} src={imgToSourceFrom} className={classNames} onError={whenImageIsMissing} />
);
};
export default Image;
It works perfectly. I have test running for it with Jest and React-Testing-Library. I have tested all but one scenario. This one:
const whenImageIsMissing = () => {
imageRef.current.src = imgToFallbackTo;
imageRef.current.onerror = () => {}; // This line.
};
This line basically prevents an infinite Loop in case both images are missing
The Problem:
I want to test that my onerror function has been called exactly one time. Which I am really stuck on how to do it. Here is the test...
const { container } = render(<Image srcImage={undefined} fallbackImage={undefined} />);
const assertion = container.querySelector('img').onerror;
fireEvent.error(container.firstElementChild);
console.log(container.firstElementChild);
expect(container.firstElementChild.ref.current.onerror).toHaveBeenCalledTimes(1);
// This though has no reference to a real value. Is an example of what I want to get at.
The Question:
How to access the ref callback function and check how many times has my function been called?
Any ideas on this. I am at a loss, I tried mocking refs, I tried mocking and spying on the component. I tried using act and async/await, in case it was called after. I really need some help on this..
You should check if your function is called or not, that's called testing implementation details, rather you should check if your img element have correct src.
Even you should add some alt and user getByAltText to select image element
const { getByAltText } = render(<Image srcImage={undefined} fallbackImage={undefined} />);
const imageElement = getByAltText('Image Alt');
fireEvent.error(imageElement);
expect(imageElement.src).toEqual(imgToFallbackTo);
You have 2 options:
Add a callback to your props that will be called when whenImageIsMissing is called:
export interface ImageProps {
srcImage: string;
classNames?: string;
fallbackImage?: FallbackImages;
onImageMissing?:();
}
const Image = ({
srcImage,
classNames,
onImageMissing,
fallbackImage = FallbackImages.FALLBACK
}: ImageProps) => {
const imgToSourceFrom = srcImage;
const imgToFallbackTo = fallbackImage;
const imageRef = useRef(null);
const whenImageIsMissing = () => {
imageRef.current.src = imgToFallbackTo;
imageRef.current.onerror = () => {};
if (onImageMissing) onImageMissing();
};
return (
<img ref={imageRef} src={imgToSourceFrom} className={classNames} onError={whenImageIsMissing} />
);
};
and then insert jest.fn in your test and check how many times it was called.
The other option is to take the implementation of whenImageIsMissing and put it inside image.util file and then use jest.spy to get number of calls. Since you are using a function component there is no way to access this function directly.
Hope this helps.

React TypeScript - passing a callback function

The following code would have worked happily in JavaScript. I am using React+TypeScript, hence this is a JSX file:
Calling component:
<FiltersPanel onFiltersChanged={() => { console.log('I was called') }} />
Inner Component:
const FiltersPanel = (onFiltersChanged) => {
const handleFiltersChange = () => {
onFiltersChanged(); /* I am getting `TypeError: onFiltersChanged is not a function` */
};
return (<div onClick={handleFiltersChange}/> );
};
export default FiltersPanel;
Why is TypeScript complaining that it cannot find the function, while the function is certainly passed as a prop.
You passed the function as a props so you should receive it to your component as a props like this
const FiltersPanel = (props) => { }
then use it like this
props.onFiltersChanged()
OR you can use destructuring like this
const FiltersPanel = ({onFiltersChanged}) => { }
so you can use it without the props object like this
onFiltersChanged()

Resources