I have an array that via useState hook, I try to add elements to the end of this array via a function that I make available through a context. However the array never gets beyond length 2, with elements [0, n] where n is the last element that I pushed after the first.
I have simplified this a bit, and haven't tested this simple of code, it isn't much more complicated though.
MyContext.tsx
interface IElement = {
title: string;
component: FunctionComponent<any>;
props: any;
}
interface IMyContext = {
history: IElement[];
add: <T>(title: string, component: FunctionComponent<T>, props: T) => void
}
export const MyContext = React.createContext<IMyContext>({} as IMyContext)
export const MyContextProvider = (props) => {
const [elements, setElements] = useState<IElement[]>([]);
const add = <T extends any>(title: string, component: FunctionComponent<T>, props: T) => {
setElements(elements.concat({title, component, props}));
}
return (
<MyContext.Provider values={{elements, add}}>
{props.children}
</MyContext.Provider>
);
}
In other elements I use this context to add elements and show the current list of elements, but I only ever get 2, no matter how many I add.
I add via onClick from various elements and I display via a sidebar that uses the components added.
SomeElement.tsx
const SomeElement = () => {
const { add } = useContext(MyContext);
return (
<Button onClick=(() => {add('test', MyDisplay, {id: 42})})>Add</Button>
);
};
DisplayAll.tsx
const DisplayAll = () => {
const { elements } = useContext(MyContext);
return (
<>
{elements.map((element) => React.createElement(element.component, element.props))}
</>
);
};
It sounds like your issue was in calling add multiple times in one render. You can avoid adding a ref as in your currently accepted answer by using the callback version of setState:
const add = <T extends any>(title: string, component: FunctionComponent<T>, props: T) => {
setElements(elements => elements.concat({title, component, props}));
};
With the callback version, you'll always be sure to have a reference to the current state instead of a value in your closure that may be stale.
The below may help if you're calling add multiple times within the same render:
interface IElement = {
title: string;
component: FunctionComponent<any>;
props: any;
}
interface IMyContext = {
history: IElement[];
add: <T>(title: string, component: FunctionComponent<T>, props: T) => void
}
export const MyContext = React.createContext<IMyContext>({} as IMyContext)
export const MyContextProvider = (props) => {
const [elements, setElements] = useState<IElement[]>([]);
const currentElements = useRef(elements);
const add = <T extends any>(title: string, component: FunctionComponent<T>, props: T) => {
currentElements.current = [...currentElements.current, {title, component, props}];
setElements(currentElements.current);
}
return (
<MyContext.Provider values={{history, add}}>
{props.children}
</MyContext.Provider>
);
}
Related
I have 2 components that pass to the child the prop data, but only one parent pass the prop numPage and the prop setNumPage,
When I try to use the optional sign in the interface it tells me that React.Dispatch<React.SetStateAction> cannot be undefined. I found a solution that is inadequate, use any, can you give me another solution?
First parent
const Home = () => {
const [searchParams] = useSearchParams()
const [numPages, setNumPages] = useState<number>(1)
const url:string = searchParams.get("search") ? `${SEARCH_URL}${searchParams.get("search")}${PAGES}${numPages}` : `${POPULAR_RESULTS}${numPages}`;
const {data, loading} = useFetch<movieApi>(url);
if(loading) return <Sppiner/>
return (
<div>
<Items
data={data}
numPages={numPages}
setNumPages={setNumPages}
/>
</div>
);
};
Second parent
const GenresPage = () => {
const { data, loading } = useFetch<movieApi>(POPULAR_RESULTS);
if(loading) return <Sppiner/>
return (
<div>
<Items data={data} />
</div>
);
};
export default GenresPage;
Child
interface DataProps {
data: movieApi | null;
numPages:number;
setNumPages:React.Dispatch<React.SetStateAction<number>>;
}
const Items = ({ data,numPages,setNumPages }:DataProps) => {}
To the child if I put any or DataProps it works, but I don't wanna do that.
You can change setNumPages inside DataProps to:
setNumPages: (value: number) => void
When designing a generic component, I adopted the principle of SOC where the generic component will not know the implementation details of the user and allow the user to listen for callbacks to handle their own state.
For example:
SortButton.tsx
interface Props {
initialValue?: boolean;
onToggle?: (value: boolean) => void;
}
const Toggle: React.FC<Props> = ({
initialValue = false,
onToggle,
}) => {
const [isActive, setIsActive] = React.useState(initialValue);
const handleToggle = React.useCallback(() => {
let updatedValue = isActive;
// do some computation if needed
if(onToggle) {
onSort(updatedValue);
}
setIsActive(updatedValue);
}, [isActive, onToggle]);
return (
<div onClick={handleToggle}>Sort</div>
);
}
Parent.tsx
const Parent: React.FC<Props> = ({
}) => {
const [parentObject, setParentObject] = React.useState({});
const handleToggle = React.useCallback((value: boolean) => {
let updatedValue = value;
// do some computation to updatedValue if needed
setParentObject((parent) => { ...parent, calculated: updatedValue });
}, []);
return (
<SortButton onToggle={handleToggle} />
);
}
The above implementation allows the generic component (ie. Toggle) to handle their own state and allows parents to implement their own implementation using callbacks. However, I have been getting the following error:
Warning: Cannot update a component (`Parent`) while rendering a different component (`Toggle`). To locate the bad setState() call inside `Toggle`
My understanding from the error is that, within the callback onClick, it triggers state mutation of the parent (in this case, another component) which should not be allowed. The solution I figured was to move the callback within an useEffect like this:
SortButton.tsx
interface Props {
initialValue?: boolean;
onToggle?: (value: boolean) => void;
}
const Toggle: React.FC<Props> = ({
initialValue = false,
onToggle,
}) => {
const [isActive, setIsActive] = React.useState(initialValue);
React.useEffect(() => {
let updatedValue = isActive;
// do some computation if needed
if(onToggle) {
onSort(updatedValue);
}
}, [isActive, onToggle]);
const handleToggle = React.useCallback(() => {
setIsActive((value) => !value);
}, []);
return (
<div onClick={handleToggle}>Sort</div>
);
}
The new implementation works but I have a few questions which I would hope to get some guidance on.
The example works and is easy to refactor because it is a simple state of isActive. What if the value we need is more complicated (for example, mouse position, etc) and does not have a state to store the value and is only available from onMouseMove? Do we create a state to store the `mouse position and follow the pattern?
Is the existing implementation an anti-pattern to any of the React concepts in the first place?
Is there any other possible implementation to solve the issue?
This is a somewhat biased opinion, but I'm a big proponent of Lifting State Up and "Dumb" Components/Controlled Components.
I would design it so that the SortButton does not have any internal state. It would get all of the information that it needs from props. The Parent would be responsible for passing down the correct value of isActive/value, which it will update when the child SortButton calls its onToggle prop.
We can include the event in the onToggle callback just in case the parent wants to use it.
SortButton.tsx
import * as React from "react";
interface Props {
isActive: boolean;
onToggle: (value: boolean, e: React.MouseEvent<HTMLDivElement>) => void;
}
const Toggle: React.FC<Props> = ({ isActive, onToggle }) => {
return (
<div
className={isActive ? "sort-active" : "sort-inactive"}
onClick={(e) => onToggle(!isActive, e)}
>
{isActive ? "Unsort" : "Sort"}
</div>
);
};
export default Toggle;
Parent.tsx
import * as React from "react";
import SortButton from "./SortButton";
interface Props {
list: number[];
}
const Parent: React.FC<Props> = ({ list }) => {
// parent stores the state of the sort
const [isSorted, setIsSorted] = React.useState(false);
// derived data is better as a memo than as state.
const sortedList = React.useMemo(
// either sort the list or don't.
() => (isSorted ? [...list].sort() : list),
// depends on the list prop and the isSorted state.
[list, isSorted]
);
return (
<div>
<SortButton
isActive={isSorted}
// you could use a more complicated callback, but it's not needed here.
onToggle={setIsSorted}
/>
<ul>
{sortedList.map((n) => (
<li>{n.toFixed(3)}</li>
))}
</ul>
</div>
);
};
export default Parent;
Code Sandbox Demo
I have a HOC like so:
utils/withGlobalSettings.tsx
interface Props {
globalSettings: {
...
};
}
export default (Component: React.ComponentType<Props>) => () => {
const locale = useContext(localeContext);
const { data } = useQuery(dataGlobalSettingsCollection);
return <Component globalSettings={globalSettings} />;
};
I used this on several simple components and it works great. The problem is when I use it on a component that expects it's own props too, like so:
components/Products.tsx
interface Props {
globalSettings: {
...
};
products: {
...
}[];
}
const Products: React.FunctionComponent<Props> = ({
globalSettings,
products,
}) => (
...
);
export default withGlobalSettings(Products);
In this case, typescript complains:
Property 'products' is missing in type 'PropsWithChildren' but
required in type 'Props'.
I believe this is because I haven't set up the HOC correctly to pass through props other than the globalSettings one generated inside the prop. How is this achieved with function component HOCs in typescript?
You need something like this:
const wrapper = <T extends Props>(Component: React.ComponentType<T>) => (ownProps: Omit<T, keyof Props>) => {
const globalSettings = { test: 2 };
const props = { globalSettings, ...ownProps };
return <Component {...props as T} />;
};
And you would use it like this:
const NewProduct = wrapper(Product);
<NewProduct products={...} />
I am pretty new to typescript. trying to build a simple crud app with react, typescript and hooks. I can't figure out how to pass data and function together as props to a child component that will further send props to his chid. Here is a sample code.
parent component
const App = () => {
const [data, setData] = useState([
{ id: 1, name: 'John', email: 'john#gmail.com' },
{ id: 1, name: 'Mike', email: 'mike#gmail.com' },
])
// how to send this function to StudentList component
const removeStudent = (id: number): void => {
setData(data.filter(item => item.id !== id))
}
return (
<div>
<StudentList data={data} removeStudent={removeStudent} />
</div>
);
}
child component / StudentList
interface StudentsInterface {
id: number,
name: string,
email: string,
}
interface PropsFunction {
removeStudent: () => void
}
const StudentList = ({ data }: { data: StudentsInterface[] }, { removeStudent }: PropsFunction) => {
console.log('removeStudent ==>', removeStudent) // getting undefined
return (
<table id='students'>
<tbody>
{data.map((student, index) => {
return (
<Student student={student} key={index} removeStudent={removeStudent} />
)
})}
</tbody>
</table>
)
}
If I just pass data (first parameter of StudentList), I get the props, but I want removeStudent function too...
If it is just react I know I would just destructure {removeStudent} in studentList and I am done, but here in typescript I have to define data type...
Hope I am clear.
Since I am pretty new to typescript I would be glad if you explain what I am doing wrong.
You're using 2 arguments as props:
const StudentList = ({ data, removeStudent }: { data: StudentsInterface[], removeStudent: PropsFunction }) => ...
EDIT:
One way to fix it is that you have defined PropsFunction as an interface, meaning it is an object with a property removeStudent - you probably just want it to be:
type PropsFunction = () => void;
I'm new to React and wondering how to change this code so that I'm not using any for the add function that is DI'd into the component.
Most of what I read says to use the React mouse click event type but that has only 1 param and isn't really what is going on anyway so seems bad two different ways.
import React, { useState } from 'react';
interface IProps {
count?: number;
incrementBy?: number;
onClick: any;
// EDIT - FIX - correct fn type
// I also took optional ? off types in app
//onClick: (count: number, incrementBy: number) => void;
}
const Description2: React.FC<IProps> = (props: IProps) => (
<div>
<p>My favorite number is {props.count}, incrementBying by {props.incrementBy}</p>
<button
onClick={() => props.onClick(props.count, props.incrementBy)}
>
Increase
</button>
</div>
);
const App: React.FC = () => {
//initialize state
const increase = 4;
const [count, setCount] = useState(increase);
const [user, setUser] = useState("world");
const add = (currentCount: number, bump: number) => {
setCount(currentCount + bump);
};
return (
<div >
<Description2
count={count}
incrementBy={increase}
onClick={add} />
</div>
);
}
export default App;
The correct type would be:
(count: number, incrementBy: number) => any