React with TypeScript: how to type useState hook with previous state - reactjs

I have a React + TS application and I'm writing a custom hook to update the header of a table. Basically, I can click on different checkboxes and hide/show that specific column of the table.
const useHeader = (headerConfiguration: TableHeader[]) => {
const [header, setHeader] = useState(headerConfiguration);
const updateHeader = (column: string) => {
setHeader(prevState => { // <- this line has the error
prevState.forEach(el => {
if (el.name === column) el.visible = !el.visible;
});
});
};
return {
header,
updateHeader,
};
};
export default useHeader;
As the code shown, I need to update the visible property based on the column name.
Currently I have an error tough:
Argument of type '(prevState: TableHeader[]) => void' is not
assignable to parameter of type 'SetStateAction<TableHeader[]>'.
Type '(prevState: TableHeader[]) => void' is not assignable to type
'(prevState: TableHeader[]) => TableHeader[]'.
Type 'void' is not assignable to type 'TableHeader[]'.
I get what the error is saying but I'm not sure how to fix it, if I write the same function using the library Immer it works without any error:
setHeader(
produce(header, draftHeader => {
draftHeader.forEach(el => {
if (el.name === column) {
el.visible = !el.visible;
}
});
}),
);
How can I fix this?

When not using immer, you're responisble to produce and return a new state. So instead of manipulating prevState you need to create a copy of it, with the changes you require applied.
For example, with something like this:
const updateHeader = (column: string) => {
setHeader(prevState => {
// return a copy prevState
return prevState.map(
el => el.name === column
? {...el, visible: !el.visible} // copy inner item, if changed
: el;
);
});
};

Please try this:
const useHeader = (headerConfiguration: TableHeader[]) => {
const [header, setHeader] = useState(headerConfiguration);
const updateHeader = (column: string) => {
header.forEach(el => {
if (el.name === column) el.visible = !el.visible;
});
setHeader(header);
};
return {
header,
updateHeader,
};
};
export default useHeader;

Related

How can I pass data from custom hook to component?

I have custom hook which pass parameter data with fetched data to component Settings. There I have hook setData11 in useEffect and I want to set in that hook data passed from useTable but I get error
Argument of type 'Data | Column[] | undefined' is not assignable to parameter of type 'SetStateAction<DataTable[] | undefined>'.
Type 'Data' is not assignable to type 'SetStateAction<DataTable[] | undefined>'.
Type 'Data' is missing the following properties from type 'DataTable[]': length, pop, push, concat, and 29 more.ts(2345)
interface DataTable {
[id: string | number]: string | number;
}
interface Data {
[category: string]: DataTable[];
}
const urls: string[] = [
"https://jsonplaceholder.typicode.com/posts",
"https://jsonplaceholder.typicode.com/comments",
"https://jsonplaceholder.typicode.com/albums",
"https://jsonplaceholder.typicode.com/photos",
"https://jsonplaceholder.typicode.com/todos",
];
const useTable = (idurl: number, actualcategory: string) => {
const [data, setData] = useState<Data>();
const [columns, setColumns] = useState<Column[]>();
const loadDatabase = () => {
fetch(urls[idurl])
.then((response) => response.json())
.then((response) => {
setData({
...data,
new: response.filter((t: DataTable[], i: number) => {
return i > 50 && i < 100 && t;
}),
removed: response.filter((t: DataTable[], i: number) => {
return i > 100 && i < 150 && t;
}),
postponed: response.filter((t: DataTable[], i: number) => {
return i > 50 && i < 100 && t;
}),
});
const objcolumn: Column[] = Object.keys(response[0]).map(
(t: string) => {
let d: Column = { col: { title: t, disp: true } };
return d;
}
);
setColumns(objcolumn);
});
};
useEffect(() => {
loadDatabase();
alert(JSON.stringify(data));
}, []);
return [data, columns];
};
export { useTable };
const Settings = () => {
const [data, columns, checkall, changeDatabase] = useTable(1, "new");
const[ data1, setData1]=useState<DataTable[]>()
useEffect(() => {
setData1(data)
}, []);
return <></>
}
It's not clear to me what this code is supposed to do, so it's difficult to give you a specific answer.
Your example contains several mistakes.
The first one can be confusing. When you return an array from your custom hook, its type will be inferred as Array<Data | Column[]>. If you want to get a signature similar to useState, you should use as const to tell TypeScript that this array contains a specific number of elements in a specific order and cannot change.
But even after you specify as const, TypeScript will complain about two things:
The number of elements that you are trying to unpack from useTable should correspond to the two that you are returning (possibly just a typo). So it's should be const [data, columns] = useTable(...)
The type Data | undefined and DataTable[] | undefined are incompatible, which is entirely true. I'm not entirely sure what your end goal is. If you want to use a specific category as the data1 state, you should pass some attribute of data
const useTable = (idurl: number, actualcategory: string) => {
...
return [data, columns] as const; // use "as const"
};
// --------------
const Settings = () => {
// Remove extra parameters
const [data, columns] = useTable(1, "new");
const [data1, setData1] = useState<DataTable[]>()
useEffect(() => {
// data also can be undefined, so you need to use ?. syntax
setData1(data?.SOME_CATEGORY)
}, []);
return <></>
}
thanks for answer :)
here is resolution of the problem
https://fettblog.eu/typescript-react-typeing-custom-hooks/

How to use values from Context Provider in Typescript? Argument of type '() => Element' is not assignable to parameter of type 'Context<unknown>'

I am trying to utilise use Context in my typescript and react app.
What I want is my Checkbox component to add Enums to a set, depending on what props have been passed to that component.
My set up is as follows:
./App.tsx
import {MyStatusEnum} from "../ts-api-scripts";
interface inputListContextType {
greenValue: { greenVals: Set<MyStatusEnum>, setGreenVals: Dispatch<SetStateAction<Set<MyStatusEnum>>> };
redValue: { redVals: Set<MyStatusEnum>, setRedVals: Dispatch<SetStateAction<Set<MyStatusEnum>>> };
}
export const inputListContext = createContext<inputListContextType>({
greenValue: { greenVals: new Set<MyStatusEnum>(), setGreenVals: () => { } },
redValue: { redVals: new Set<MyStatusEnum>(), setRedVals: () => { } },
});
type checkBoxParams = {
myEnum: any
statusName: any
}
export const Options = () => {
const [greenVals, setGreenVals] = useState(new Set<MyStatusEnum>());
const [redVals, setRedVals] = useState(new Set<MyStatusEnum>());
const greenValue = {greenVals, setGreenVals};
const redValue = {redVals, setRedVals};
return (
<inputListContext.Provider value={{greenValue, redValue}}>
<Checkbox myEnum={MyStatusEnum.Option1} name={"Option 1"}></Checkbox>
</inputListContext.Provider>
)
}
As far as I am aware I have defined the interface and types correctly, I enquired how to do that in my previous question here.
The issue comes from when I try to access my green and red values from within the component that my Context Provider is wrapping.
./Checkbox
import inputListContext from '../App';
interface Props {
myEnum: any
name: any
}
const Checkbox = ({ myEnum, statusName } :Props) => {
const { greenValue, redValue} = useContext(inputListContext);
return (
<div className="title">{statusName}</div>
</div>
);
};
export default Checkbox;
I am trying to access the values of my context and eventually trigger a state change from within each component.
This is my error report:
ERROR in src/components/Checkbox.tsx:35:13
TS2339: Property 'greenValue' does not exist on type 'unknown'.
> 35 | const { greenValue, redValue} = useContext(inputListContext);
| ^^^^^^^^^^
ERROR in src/components/Checkbox.tsx:35:25
TS2339: Property 'redValue' does not exist on type 'unknown'.
> 35 | const { greenValue, redValue} = useContext(inputListContext);
| ^^^^^^^^
ERROR in src/components/Checkbox.tsx:35:48
TS2345: Argument of type '() => Element' is not assignable to parameter of type 'Context<unknown>'.
> 35 | const { greenValue, redValue} = useContext(inputListContext);
| ^^^^^^^^^^^^^^^
^
Do I need to define additional types in order to use this context correctly. Why does typescript have these Errors?

nested store function is not a function at runtime (zustand store, immer, persist, typescript)

I've got a problem in a next.js typescript project where i'm using zustand store with persist and immer: https://github.com/pmndrs/zustand
If the toggle_slide_over function is outside of the session_setup object, it works.
If it is inside, it throws runtime error 'not a function'.
Why & how do i fix this?
Interface:
export interface MyAppState {
session_setup: {
slide_over_open: boolean,
toggle_slide_over: (new_toggle_state: boolean) => void,
...
},
session_setup_toggle_slide_over: (new_toggle_state: boolean) => void,
...
}
Function declarations inside doPersist():
const doPersist: () any => { return persist((set) => ({
session_setup: {
...
toggle_slide_over: (new_toggle_state) => set(
produce(state => { state.session_setup.slide_over_open = new_toggle_state })
),
},
session_setup_toggle_slide_over: (new_toggle_state) => set(
produce(state => { state.session_setup.slide_over_open = new_toggle_state })
),
...
How they are retrieved in React:
// When nested:
const toggle_slide_over = useMyAppStore(state => state.session_setup.toggle_slide_over)
// When not nested:
const toggle_slide_over = useMyAppStore(state => state.session_setup_toggle_slide_over)
How they are used:
onClick={() => toggle_slide_over(new_state)}
Store:
const initializeStoreByEnv = (): any => {
if (process.env.NODE_ENV === 'development') return devtools(doPersist())
else return doPersist()
}
export const useMyAppStore = create<MyAppState>()(
initializeStoreByEnv()
)
I've searched for related zustand/immer/redux/functions nested in objects/function not a function at runtime errors but haven't found anything useful, yet...
I'm working around this by just prefixing out-of-object functions with 'session_setup_', which is ok atm, but will get xxxl names with another level of nesting objects.
I completely ran into the same issue. I had nested slices, containing data and functions, which i wanted to persist using zustand/persist. I ended up going with this following approach:
src/zustand/store/index.ts
export const useMyAppStore = create<MyAppState>()(
persist(
(...args) => ({
foo: createFooSlice(...args)
}),
{
name: "app-store",
merge: (persistedState, defaultState) => {
if (!persistedState || typeof persistedState !== "object") {
return defaultState;
}
let resultState: MyAppState = { ...defaultState };
const keys = Object.keys(defaultState) as (keyof MyAppState)[]
keys.forEach((key) => {
if (key in persistedState) {
//#ts-ignore // TypeScript currently don't recognize that key exists in localState
const state = persistedState[key]
if (!!state) {
resultState = { ...resultState, [key]: { ...defaultState[key], ...state }}
}
}
})
return resultState;
}
}
)

RTK Query UseLazyQuery and Typescript: how to get the correct type from hook argument

I need to create a react hook that accepts a RTK Query UseLazyQuery function (from any endpoint), apply a debounce to it and return the trigger function and results.
Everything is working as expected but I can't manage to get the correct type to the data returned by it.
As "UseLazyQuery" type requires a type for its generic type and I don't know what type it'll be, I set it to "any", but it makes the data to be typed as "any" too.
I need help to get/extract the correct type from the given "UseLazyQuery" function given to the hook and make the returned data have the correct type.
export default function useSearch<T extends UseLazyQuery<any>>(lazyQueryFn: T) {
const [ trigger, results ] = lazyQueryFn()
const getParamsAndTrigger = async (
params: ISearchParamsRequest,
filterParams?: (params: ISearchParamsRequest) => ISearchParamsRequest
) => {
await trigger(filterParams ? filterParams(params) : params, true)
}
const debouncedTrigger = useDebouncedFn(getParamsAndTrigger, 1500)
return [debouncedTrigger, { ...results }] as const
}
The "results" const returned by "lazyQueryFn" has the type "UseQueryStateDefaultResult". I need it to have the right data type.
I had a similar situation this days and I came with this approach which fixed it and should help you too:
interface ResponseData<V> {
items: V[];
cursor: string;
}
export const useGetCursorPaginatedData = <
V,
T extends UseLazyQuery<QueryDefinition<any, any, any, ResponseData<V>, any>>
>(
lazyQuery: T
) => {
const [trigger] = lazyQuery();
const { onTriggerAction, isLoading, error } = useAsyncAction();
const [items, setItems] = useState<V[]>([]);
const [newCursor, setNewCursor] = useState<string | undefined>();
const getData = async (isRefresh?: boolean) => {
const tempList = isRefresh ? [] : items;
const { data } = await trigger(isRefresh ? undefined : newCursor);
setItems([...tempList, ...data?.items]);
setNewCursor(data?.cursor);
};
const onLoadMore = useCallback(() => {
getData();
}, [newCursor]);
const onRefresh = () => {
onTriggerAction(async () => getData(true));
};
useEffect(() => {
onTriggerAction(getData);
}, []);
return {
items,
isLoading,
isError: error != null,
onLoadMore,
onRefresh
};
};
And this is how to use it:
const {
items: dailyTweets,
isLoading,
isError,
onLoadMore,
onRefresh
} = useGetCursorPaginatedData<DailyTweet, typeof useLazyGetDailyTweetsQuery>(
useLazyGetDailyTweetsQuery
);
Basically what you need extra is to define an interface for the QueryDefinition Response, in my case ResponseData and add one more generic type to function definition that is the type of the returned data in your response.
I hope this will help you.

How to check "Type" user passed in custom hook in React typescript?

I am creating a custom hook to fetch json from api. I am using this hook for different type of data. So i need to set the type whenever i use this hook. Based on the type I use hook should parse data and return the parsed data which should contain data of our desired type.
const useSearch = <T>(url: string) => {
const [data, setData] = useState<T[]>([])
const fetchData = React.useCallback(
(searchText: string): void => {
fetch(url).then(res=>res.json()).then(({ data }) => {
//check if T is Type1, Type2, Type3
// all types parse their data using different functions
//I need to check the type of T
setData(parseData(data))
}).catch(error z=> {
console.log(error)
})
},
[],
)
return [data, fetchData] as const
}
But I am unable to find a way to check the type of T and use the corresponding parsing function.
You could try using type predicates:
type MyDataType = {
foo: 'bar'
}
type MyOtherDataType = {
baz: 'biz'
}
function isMyDataType(obj: MyDataType | MyOtherDataType): obj is MyDataType {
return (obj as MyDataType).foo !== undefined;
}
function isMyOtherDataType(obj: MyDataType | MyOtherDataType): obj is MyOtherDataType {
return (obj as MyOtherDataType).baz !== undefined;
}
// Now your hook...
const useSearch = <T>(url: string) => {
const [data, setData] = useState<T[]>([])
const fetchData = React.useCallback(
(searchText: string): void => {
fetch(url).then(res=>res.json()).then(({ data }) => {
if (isMyDataType(data)) {
// TS will treat data as MyDataType
myUniqueDataHandler(data);
} else if (isMyOtherDataType(data)) {
// TS will treat data as MyOtherDataType
myOtherUniqueDataHandler(data);
}
setData(parseData(data))
}).catch(error z=> {
console.log(error)
})
},
[],
)
return [data, fetchData] as const
}
Alternatively, you could add a "type" field to the data objects that are returned by your API and have an easy check at runtime. Type predicates have the advantage of being type safe (or, at least, as type safe as your predicates); in other words, TS will treat your data as the type you've narrowed to AND Javascript will perform the same check at runtime so you can run your unique handlers.

Resources