Invalid Hook Call - useState function - reactjs

I am new to react js and running into below error:
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component. This could happen for one of the following
reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug
and fix this problem
The line where it fails:
const [scale, setScale] = useState(initialScale);
Code snippet is here:
import React, {
useMemo,
useState,
useEffect,
useCallback,
useImperativeHandle,
forwardRef,
Ref,
} from 'react';
import usePDF from './hooks/usePDF';
import useAnnotations from './hooks/useAnnotations';
import useTextMap from './hooks/useTextMap';
import Page from './components/Page';
import Error from './components/Error';
import ButtonGroup from './components/ButtonGroup';
import { Entity } from './interfaces/entity';
import { Annotation } from './interfaces/annotation';
import { TextLayer, TextLayerItem } from './interfaces/textLayer';
import { debug } from 'console';
interface Props {
url?: string;
data?: Uint8Array | BufferSource | string;
httpHeaders?: {
[key: string]: string;
};
initialScale?: number;
tokenizer?: RegExp;
disableOCR?: boolean;
entity?: Entity;
initialTextMap?: Array<TextLayer>;
defaultAnnotations?: Array<Annotation>,
getAnnotations(annotations: Array<Annotation>): void
getTextMaps?(textMaps: Array<TextLayer>): void;
}
const Annotator = forwardRef(({
url,
data,
httpHeaders,
initialScale = 1.5,
tokenizer = new RegExp(/\w+([,.\-/]\w+)+|\w+|\W/g),
disableOCR = false,
entity,
initialTextMap,
defaultAnnotations = [],
getAnnotations,
getTextMaps,
}: Props, ref?: Ref<any>) => {
const [scale, setScale] = useState(initialScale);
const { pages, error, fetchPage } = usePDF({ url, data, httpHeaders });
const {
annotations,
getAnnotationsForPage,
addAnnotation,
removeAnnotation: deleteAnnotation
} = useAnnotations(defaultAnnotations);
const { textMap, addPageToTextMap } = useTextMap(annotations);
useImperativeHandle(ref, () => ({ removeAnnotation }));
const removeAnnotation = (id: string) => {
deleteAnnotation(id);
};
useEffect(() => {
if (getAnnotations) {
getAnnotations(annotations);
}
if (getTextMaps) {
getTextMaps(initialTextMap || textMap);
}
}, [annotations, textMap, initialTextMap, getAnnotations, getTextMaps]);
const getTextLayerForPage = useCallback((page: number): Array<TextLayerItem> | undefined => {
if (initialTextMap) {
const found = initialTextMap.find((layer) => layer.page === page);
return found ? found.textMapItems : undefined;
}
return undefined;
}, [initialTextMap]);
const renderPages = useMemo(() => {
if (!url && !data) {
return (
<Error
message="You need to provide either valid PDF data or a URL to a PDF"
/>
);
}
console.log('i am here')
debugger
// if (error) {
// return <Error />;
// }
return (
Array(pages).fill(0).map((_, index) => {
const key = `pdf-page-${index}`;
const pageNumber = index + 1;
const page = fetchPage(pageNumber);
return (
<Page
page={page}
scale={scale}
key={key}
tokenizer={tokenizer}
disableOCR={disableOCR}
pageNumber={pageNumber}
annotations={getAnnotationsForPage(pageNumber)}
addAnnotation={addAnnotation}
removeAnnotation={deleteAnnotation}
addPageToTextMap={addPageToTextMap}
entity={entity}
initialTextLayer={getTextLayerForPage(pageNumber)}
/>
);
})
);
}, [
url, data, pages, error, scale, tokenizer, disableOCR, entity,
fetchPage, getAnnotationsForPage, addAnnotation, deleteAnnotation, addPageToTextMap, getTextLayerForPage,
]);
return (
<div className="annotator-container">
<div className="annotator-pages-container">
<div className="annotator-pages">
{ renderPages }
</div>
</div>
<ButtonGroup scale={scale} setScale={setScale} />
</div>
);
});
export default Annotator;

Related

Type errors when extending component more than one level using forwardRef and useImperativeHandle

I'm experimenting with extending components in React. I'm trying to extend Handsontable using forwardRef and useImperativeHandle. First I wrap Handsontable in my own BaseTable component, adding some methods. Then I extend the BaseTable in a CustomersTable component in the same way to add even more methods and behavior. Everything seems to work well until I try to consume the CustomersTable in CustomersTableConsumer where I get some type errors. The component works just fine, it's just Typescript that isn't happy.
BaseTable:
export type BaseTableProps = {
findReplace: (v: string, rv: string) => void;
} & HotTable;
export const BaseTable = forwardRef<BaseTableProps, HotTableProps>(
(props, ref) => {
const hotRef = useRef<HotTable>(null);
const findReplace = (value: string, replaceValue: string) => {
const hot = hotRef?.current?.__hotInstance;
// ...
};
useImperativeHandle(
ref,
() =>
({
...hotRef?.current,
findReplace
} as BaseTableProps)
);
const gridSettings: Handsontable.GridSettings = {
autoColumnSize: true,
colHeaders: true,
...props.settings
};
return (
<div>
<HotTable
{...props}
ref={hotRef}
settings={gridSettings}
/>
</div>
);
}
);
CustomersTable:
export type CustomerTableProps = HotTable & {
customerTableFunc: () => void;
};
export const CustomersTable = forwardRef<CustomerTableProps, BaseTableProps>(
(props, ref) => {
const baseTableRef = useRef<BaseTableProps>(null);
const customerTableFunc = () => {
console.log("customerTableFunc");
};
useImperativeHandle(
ref,
() =>
({
...baseTableRef?.current,
customerTableFunc
} as CustomerTableProps)
);
useEffect(() => {
const y: Handsontable.ColumnSettings[] = [
{
title: "firstName",
type: "text",
wordWrap: false
},
{
title: "lastName",
type: "text",
wordWrap: false
}
];
baseTableRef?.current?.__hotInstance?.updateSettings({
columns: y
});
}, []);
return <BaseTable {...props} ref={baseTableRef} />;
}
);
CustomerTableConsumer:
export const CustomerTableConsumer = () => {
const [gridData, setGridData] = useState<string[][]>([]);
const customersTableRef = useRef<CustomerTableProps>(null);
const init = async () => {
const z = [];
z.push(["James", "Richard"]);
z.push(["Michael", "Irwin"]);
z.push(["Solomon", "Beck"]);
setGridData(z);
customersTableRef?.current?.__hotInstance?.updateData(z);
customersTableRef?.current?.customerTableFunc();
customersTableRef?.current?.findReplace("x", "y"); };
useEffect(() => {
init();
}, []);
// can't access extended props from handsontable on CustomersTable
return <CustomersTable data={gridData} ref={customersTableRef} />;
};
Here is a Codesandbox example.
How do I need to update my typings to satisfy Typescript in this scenario?
You need to specify the type of the ref for forwardRef. This type is used then later in useRef<>().
It's confusing, because HotTable is used in useRef<HotTable>(), but BaseTable can't be used the same way, as it is a functional component and because forwardRef was used in BaseTable. So, basically, for forwardRef we define a new type and then later use that in useRef<>(). Note the distinction between BaseTableRef and BaseTableProps.
Simplified example
export type MyTableRef = {
findReplace: (v: string, rv: string) => void;
};
export type MyTableProps = { width: number; height: number };
export const MyTable = forwardRef<MyTableRef, MyTableProps>(...);
// then use it in useRef
const myTableRef = useRef<MyTableRef>(null);
<MyTable width={10} height={20} ref={myTableRef} />
Final solution
https://codesandbox.io/s/hopeful-shape-h5lvw7?file=/src/BaseTable.tsx
BaseTable:
import HotTable, { HotTableProps } from "#handsontable/react";
import { registerAllModules } from "handsontable/registry";
import { forwardRef, useImperativeHandle, useRef } from "react";
import Handsontable from "handsontable";
export type BaseTableRef = {
findReplace: (v: string, rv: string) => void;
} & HotTable;
export type BaseTableProps = HotTableProps;
export const BaseTable = forwardRef<BaseTableRef, BaseTableProps>(
(props, ref) => {
registerAllModules();
const hotRef = useRef<HotTable>(null);
const findReplace = (value: string, replaceValue: string) => {
const hot = hotRef?.current?.__hotInstance;
// ...
};
useImperativeHandle(
ref,
() =>
({
...hotRef?.current,
findReplace
} as BaseTableRef)
);
const gridSettings: Handsontable.GridSettings = {
autoColumnSize: true,
colHeaders: true,
...props.settings
};
return (
<div>
<HotTable
{...props}
ref={hotRef}
settings={gridSettings}
licenseKey="non-commercial-and-evaluation"
/>
</div>
);
}
);
CustomersTable:
import Handsontable from "handsontable";
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef
} from "react";
import { BaseTable, BaseTableRef, BaseTableProps } from "./BaseTable";
export type CustomerTableRef = {
customerTableFunc: () => void;
} & BaseTableRef;
export type CustomerTableProps = BaseTableProps;
export const CustomersTable = forwardRef<CustomerTableRef, CustomerTableProps>(
(props, ref) => {
const baseTableRef = useRef<BaseTableRef>(null);
const customerTableFunc = () => {
console.log("customerTableFunc");
};
useImperativeHandle(
ref,
() =>
({
...baseTableRef?.current,
customerTableFunc
} as CustomerTableRef)
);
useEffect(() => {
const y: Handsontable.ColumnSettings[] = [
{
title: "firstName",
type: "text",
wordWrap: false
},
{
title: "lastName",
type: "text",
wordWrap: false
}
];
baseTableRef?.current?.__hotInstance?.updateSettings({
columns: y
});
}, []);
return <BaseTable {...props} ref={baseTableRef} />;
}
);
CustomerTableConsumer:
import { useEffect, useRef, useState } from "react";
import { CustomersTable, CustomerTableRef } from "./CustomerTable";
export const CustomerTableConsumer = () => {
const [gridData, setGridData] = useState<string[][]>([]);
const customersTableRef = useRef<CustomerTableRef>(null);
// Check console and seee that customerTableFunc from customersTable,
// findReplace from BaseTable and __hotInstance from Handsontable is available
console.log(customersTableRef?.current);
const init = async () => {
const z = [];
z.push(["James", "Richard"]);
z.push(["Michael", "Irwin"]);
z.push(["Solomon", "Beck"]);
setGridData(z);
customersTableRef?.current?.__hotInstance?.updateData(z);
customersTableRef?.current?.customerTableFunc();
};
useEffect(() => {
init();
}, []);
return <CustomersTable data={gridData} ref={customersTableRef} />;
};
In your sandbox example it's almost correct, just fix the props type for CustomersTable. I would recommend though to not use Props suffix for ref types, as it is very confusing.
https://codesandbox.io/s/unruffled-framework-1xmltj?file=/src/CustomerTable.tsx
export const CustomersTable = forwardRef<CustomerTableProps, HotTableProps>(...)

TypeError: getItems[props.action] is not a function

I'm new to react testing, i'm trying to verify react functional component to be there, with this code, but for some reason it is complaining about something else TypeError: getItems [ props.action ] is not a function
any help/suggestion with this, to verify component to be there ?
interface SelectionListProps {
actionArgs?: string | undefined;
action: string;
name?: string;
identifier?: string;
classes: {
button_basic: string;
formControl: string;
selectionCard: string;
};
}
export const SelectionList: React.FC<SelectionListProps> = (
props
) => {
const dispatch = useDispatch();
const getItems = {
analysers: (site: any) => dispatch(getAnalysers(site)),
sites: (site: any) => dispatch(getSites()),
} as any;
const initializeCollapses = () => {
};
useEffect(() => {
getItems[props.action](props.actionArgs).then(() => {
initializeCollapses();
});
}, []);
return (
<List component="div" data-testid="SelectionListt">
</List>
);
};
my testing code:
import React from "react";
import { expect } from "#jest/globals";
import { render, screen, cleanup, fireEvent } from "#testing-library/react";
import { SelectionList } from "../SelectionList";
import { Provider } from "react-redux";
import { store } from "../../redux/store";
import "#testing-library/jest-dom/extend-expect";
function handleUpdateClick(event: any, type = "") {}
test("test", () => {
render(
<Provider store={store}>
<SelectionList
classes={{ button_basic: "", formControl: "", selectionCard: "" }}
action={""}
actionArgs={""}
/>
</Provider>
);
const SelectionCardElement = screen.getByTestId("SelectionListt");
expect(SelectionCardElement).toBeInTheDocument();
});
You are passing an empty string for the action name, but getItems[""] does not match to any function since the value of getItems is:
const getItems = {
analysers: (site: any) => dispatch(getAnalysers(site)),
platforms: (site: any) => dispatch(getPlatforms(site)),
brokers: (site: any) => dispatch(getBrokers(site)),
cameras: (site: any) => dispatch(getCameras(site)),
sites: (site: any) => dispatch(getSites()),
}
Also all functions of getItems, but the last one, expect a site argument. However currently you are passing an empty string as well for the arguments. In the sites function you can remove the unused sites argument

React freezing when updating context

using react hooks
Experiencing a weird bug that seems to cause some sort of infinite loop or something. Has anyone run into something like this before.
Here's what I have:
import { createContext, useState, useCallback } from "react";
export type ModalValue = string | null;
const DEFAULT_ACTION = null;
export const useModalContext = (): ModalContextContents => {
const [
modal,
setModalInner,
] = useState<ModalValue>(DEFAULT_ACTION);
const setModal = useCallback((nv: ModalValue) => {
setModalInner(nv);
}, []);
return {
modal,
setModal,
};
};
interface ModalContextContents {
modal: ModalValue;
setModal: (nv: ModalValue) => void;
}
export const ModalContext = createContext<ModalContextContents>({modal: null, setModal: () => {}});
modal.tsx
import React, {useContext, useCallback} from 'react';
import {Box, Button, Aligner} from 'components';
import {ModalContext} from './modalContext';
import './modal.scss';
export const Modal = () => {
const modalApi = useContext(ModalContext);
if (modalApi.modal === null) {
return <></>
}
return <div key="lobby-modal" className='modal'>
<Aligner>
<Box style={{background: 'rgba(0, 0, 0, 0.72)'}}>
{modalApi.modal || ''}
<Button text="close" onClick={() => {modalApi.setModal(null)}}/>
</Box>
</Aligner>
</div>;
}
For some reason when I call:
modalApi.setModal('some-text');
then:
modalApi.setModal(null);
the entire page freezes.
Anyone have any idea what's going on here?
elsewhere in the app I had:
const callbackMapping: {[key: string]: Action} = useMemo(() => {
return {
callback: () => modelApi.setModal('wardrobe')
}
}, [room, modelApi])
then I was invoking it in a useEffect.
so I was causing an infinite in the app by changing the context value and then re-changing it.

React Typescript passing props and data type

I'm still pretty new into the world of TypeScript and I have a couple of questions. I have a few errors in my code, and I'm not rlly sure how to pass the props and choose the right type. I would appreciate a bit of help on that one.
Do you have maybe a good source where I can find all the necessary React info in one place just for the start?
tl;dr
What will be the { data } type? How to define it?
How to pass the functions as props to the Results.tsx file? How define result, results and openPopup in this function?
Did I miss something else?
App.tsx
import React, { useState } from 'react'
import axios from 'axios'
import Search from './components/Search'
import Results from './components/Results'
import Popup from './components/Popup'
export type selected = {
Title?: string,
Year?:number
}
type values = {
s: string,
results: string[],
selected: selected,
}
interface popup {
id: string
}
const App: React.FC = () => {
const [state, setState] = useState<values>({
s: "",
results: [],
selected: {}
});
const apiurl = "http://www.omdbapi.com/?apikey=dfe6d885";
const search = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
axios(apiurl + "&s=" + state.s).then(({ data }) => {
let results = data.Search;
setState(prevState => {
return { ...prevState, results: results }
})
});
}
}
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
let s = e.target.value;
setState(prevState => {
return { ...prevState, s: s }
});
}
const openPopup = (id : string) => {
axios(apiurl + "&i=" + id).then(({ data }) => {
let result = data;
console.log(result);
setState(prevState => {
return { ...prevState, selected: result }
});
});
}
const closePopup = () => {
setState(prevState => {
return { ...prevState, selected: {} }
});
}
return (
<div className="App">
<header>
<h1>Movie Database</h1>
</header>
<main>
<Search handleInput={handleInput} search={search} />
<Results results={state.results} openPopup={openPopup} />
{(typeof state.selected.Title != "undefined") ? <Popup selected={state.selected} closePopup={closePopup} /> : false}
</main>
</div>
);
}
export default App
Results.tsx
import React from 'react'
import Result from './Result'
function Results ({ results, openPopup }) {
return (
<section className="results">
{results.map(result => (
<Result key={result.imdbID} result={result} openPopup={openPopup} />
))}
</section>
)
}
export default Results
Prop Types
You can defined the props that you pass to a function component inline
function Results ({ results, openPopup }: { results: MyResult[], openPopup: () => void }) {
But it's more common to define a separate interface for the props
interface Props {
results: MyResult[];
openPopup: () => void;
}
Which you can use to define the props directly
function Results ({ results, openPopup }: Props) {
Or through React.FC
const Results: React.FC<Props> = ({ results, openPopup }) => {
Data Type
To define the type for data you need to create an interface that has all of the properties in the API response that you want to use. Then when you call axios, you use the generic type of the axios function to tell it what the fetched data should look like.
For whatever reason, axios() doesn't seem to take a generic, but axios.get() does.
axios.get<MyApiResponse>(apiurl + "&s=" + state.s).then(({ data }) => {
Now the data variable automatically has the MyApiResponse type.

How to update custom hook value

I've started learning typescript and react hooks on my new web project. I have some problem with managing initial value of my custom hook i've made. Cant step over this functionality. How should I update nameInput hook, after data is fetched from my API. For example if I pass custom string as initial value in useTextInput everything is fine, but problem is when im passing undefined first there(when data is fetched from API) and value after is downloaded, and seems like it is not updating.
My question is, how to update in AccountInput inputValue value properly by value coming from API as userAccount.firstName from getUser() method, witch depends to nameInput, witch is not updated after initial value in useTextInput.
Account.tsx
import React, { useEffect, useState} from 'react';
import { connect } from 'react-redux';
import { IApplicationState } from '../../store';
import { actionCreators, reducer, AccountStatusEnum } from '../../store/Account';
import { MDBBtn, MDBInput } from 'mdbreact';
import { AccountInput } from './child-components';
import { useTextInput } from '../../hooks';
type AccountProps = ReturnType<typeof reducer> & typeof actionCreators;
const Account: React.FC<AccountProps> = ({ getUser, resetState, userAccount, status }) => {
const nameInput = useTextInput(userAccount.firstName);
const [isInputInvalid, setIsInputInvalid] = useState<boolean>(false);
useEffect(() => {
getUser();
}, []);
return (
<React.Fragment>
<div className="mt-3 container border border-light">
<h5 className="secondary-heading font-weight-bold pt-3 pl-5">User info</h5>
<div className="row">
<AccountInput
textInput={nameInput}
typeInput="text"
labelInput="Your name & surename"
isInputInvalid={isInputInvalid}
/>
</div>
</div>
</React.Fragment>
);
};
const mapStateToProps = (state: IApplicationState) => state.acc;
export default connect(mapStateToProps, actionCreators)(Account as any);
AccointInput.tsx
import React, { Fragment, useEffect, useState } from 'react';
import { TextInput } from '../../../hooks';
import { MDBInput } from 'mdbreact';
type AccountInputProps = {
readonly textInput: TextInput;
readonly isInputInvalid: boolean;
readonly typeInput: string;
readonly labelInput: string;
};
const AccountInput = React.memo<AccountInputProps>(({ textInput, isInputInvalid, typeInput, labelInput }) => {
const { hasValue, bindToInput } = textInput;
return (
<Fragment>
<div className="col-sm w-100 px-5">
<MDBInput hint={textInput.value} {...bindToInput} type={typeInput} label={labelInput} />
</div>
</Fragment>
);
});
AccountInput.displayName = 'AccountInput';
export default AccountInput;
useTextInput.ts
import { useState, useCallback, useMemo, FormEvent } from 'react';
export type TextInputType = 'text' | 'password';
export type TextInput = {
value: string;
hasValue: boolean;
clear: () => void;
bindToInput: {
value: string;
type: TextInputType;
onChange: (e: FormEvent<HTMLInputElement>) => void;
};
};
export const useTextInput = (initial: string = '', type: TextInputType = 'text'): TextInput => {
const [value, setValue] = useState<string>(initial);
const clear = useCallback((): void => setValue(''), []);
const onChange = useCallback((e: FormEvent<HTMLInputElement>): void => setValue(e.currentTarget.value), []);
return useMemo<TextInput>(
() => ({
value,
clear,
hasValue: !!(value && value.trim()),
bindToInput: {
type,
value,
onChange,
},
}),
[value, type, onChange, clear],
);
};
I don't know for sure how custom hooks handle promises, but normally when you want to pass an async value to useState you do something like this:
const useTextInput = (initial) => {
const [value, setValue] = useState(initial);
useEffect(() => {
setValue(initial);
}, [initial]);
};
You can use useEffect to update your stateful value when initial changes. Would this work in your case?

Resources