In my expo typescript app, I have a function in a root component:
const getCardType = (type = ECardType.WOOD) => {
setCardType(type);
};
and pass it in my first child component:
<Slider data={slideData} autoPlay={false} getCardType={getCardType} />
Here is my first child component where I pass the function, and it types declaration:
readonly getCardType?: (type: ECardType) => void;
const Slider: React.FunctionComponent<ISliderProps> = ({
data,
autoPlay,
getCardType,
})
After that I pass it into a second child component:
<SliderCardItem cardItem={item} index={index} getCardType={getCardType} />
And in this SliderItem component, I use this function:
useEffect(() => {
getCardType(cardType);
}, [cardType]);
But Have a TS error: Cannot invoke an object which is possibly 'undefined' in children component
I set the cardType below in an onPress()
I have this error only in this component
An idea to fix this error?
getCardType might be undefined, as stated in your type here:
getCardType?: (type: ECardType) => void;
Then you're trying to call it without checking if it exists:
useEffect(() => {
getCardType(cardType);
}, [cardType]);
So you'll need to perform that check:
useEffect(() => {
if (getCardType) getCardType(cardType);
}, [cardType]);
or with optional chaining:
useEffect(() => {
getCardType?.(cardType);
}, [cardType]);
If it will always be present then you can make it non optional in your type:
getCardType: (type: ECardType) => void;
Remove the ? in the type declaration, if it's required. If it's not required, you have to check if it exists first.
Another point, getCardType is actually a dependency of that effect as well, but by looking at it it's safe, to ignore it (because it just wraps a setState) call.
I don't like ignoring stuff, though. So if I were you, I'd probably write it like this:
// useCallback makes getCardType referentially identical between renders
const getCardType = useCallback((type = ECardType.WOOD) => {
setCardType(type);
// safe to ignore setCardType in the dependencies because it's a dispatcher
},[]);
// ... and in the child:
useEffect(() => {
getCardType(cardType);
}, [ getCardType, cardType ]);
I'm honestly dying to know what that useEffect is about in the child, because it smells a little fishy.
Related
I have a component with a save button
<Button onClick={() => requestSaveAsync()}>Save</Button>
I also need to be able to call requestSaveAsync from the top level of the app, so I added a useImperativeHandle hook inside a custom hook called useEditRow
interface Props {
ref: MutableRefObject<{
addItem: () => void
}>
}
const useEditRow = (props: Props) => {
useImperativeHandle(props.ref, () => ({
addItem: () => requestSaveAsync()
})
}
The useEditRow hook is inside of an OrderItemTable component
const OrderItemTable = forwardRef(function OrderItemTable(props: Props, ref: MutableRefObject<any>) {
const editRow = useEditRow({
ref: ref
})
})
The requestSaveAsync method uses useMutation from react-query
useMutation(mutateFn, {
onSuccess: () => dispatch({type: 'clear', name: 'row'})
})
Clear row sets the state to initial state. If requestSaveAsync is called by clicking the button, the row is cleared. If I call it through the parent component, the onSuccess function is called, but the dispatch doesn't do anything. If I put a breakpoint on the dispatch function, I see the following code about to called from react_devtools_backend.js
useReducer: function (a, b, e) {
a = F();
b = null !== a ? a.memoizedState : void 0 !== e ? e(b) : b;
z.push({
primitive: "Reducer",
stackError: Error(),
value: b
});
// devtools show the empty function on this line will be executed next
return [b, function () {}];
},
At first I thought that maybe useImperativeHandle was using stale state, so I tried returning {...initialState} instead of initialState. This didn't seem to help. I tried adding the dependencies array suggested by react-hooks/exhaustive-dep. That didn't help. Does anyone know why when dispatch is called from useImperativeHandle, the state doesn't update?
Here is a codesandbox with some of the basic ideas that were shown abo.
Your useImperativeHandle hook doesn't appear to be using the forwarded React ref. In other words, React refs are not regular React props that can be accessed by children components.
You should use React.forwardRef to correctly forward any passed refs on to the function component.
React.forwardRef
Forwarding Refs
You didn't include your function component but if you follow the examples in the links provided it's fairly trivial to figure out.
Example:
const MyComponentWithSaveButton = (props, ref) => {
useImperativeHandle(ref, () => ({
addItem: requestSaveAsync,
}));
const requestSaveAsync = () => { .... };
...
};
export default React.forwardRef(MyComponentWithSaveButton);
Notice here the function component signature not accepts two arguments, the props object and the React ref that is being forwarded, and that the useImperativeHandle hook references the forwarded ref.
Anyone experienced with this case, I don't know how to declare the Select properties in Typescript. Please help!
import React from 'react'
import Select, {components, IndicatorProps} from 'react-select'
import {connect} from 'react-redux'
import ArrowDropDownIcon from '#material-ui/icons/ArrowDropDown';
import { Users } from '../../../Redux/Users/UsersReducer';
import { RootState } from '../../../Redux/rootReducer';
interface SelectButtonProps {
onSelect: (event: React.FormEvent<HTMLSelectElement>) => void,
users: Users[],
stateId: string
}
const SelectUserButton:React.FunctionComponent<SelectButtonProps> = ({onSelect, users, stateId}): JSX.Element => {
const options = users.map( (user:Users) => ({label: user.name, value:`${user.id}`}))
const DropdownIndicator = (
props : IndicatorProps<any>
) => {
return (
<components.DropdownIndicator {...props}>
<ArrowDropDownIcon id="arrow_icon"/>
</components.DropdownIndicator>
);
};
const SingleValue = ({ children, ...props }:any) => (
<components.SingleValue {...props}>
{children}
</components.SingleValue>
);
return (
<div className='select__user'>
<label htmlFor='react-select-container'>Users</label>
<Select
SingleValue={SingleValue}
name="user"
components={{ DropdownIndicator }}
options={options}
onChange={onSelect}
maxMenuHeight={120}
placeholder={stateId}
/>
</div>
)
}
I got an error on the onChange event, because when using react-select, it will return an object.
And the browser also shows this error: "
The above error occurred in the <WithStyles(ForwardRef(SvgIcon))> component:
in WithStyles(ForwardRef(SvgIcon))
in ForwardRef
in ForwardRef (at SelectUserButton.tsx:23)
in div (created by Context.Consumer)
in EmotionCssPropInternal (created by DropdownIndicator)
in DropdownIndicator (at SelectUserButton.tsx:22)"
Let's start with the easy errors/issues which are to do with SingleValue.
Your const SingleValue is passing through all of the props it receives so it's not doing anything. You can write const SingleValue = components.SingleValue and it will be the same thing. You can also pass components.SingleValue directly without assigning it to a local variable.
I've had a look at the docs and source code for "react-select" and it doesn't seem like SingleValue is a prop of Select, though oddly this does not trigger a typescript error. Instead it should be an additional property on components alongside DropdownIndicator. It would be components={{ DropdownIndicator, SingleValue: components.SingleValue }}, but since you aren't changing anything from the default, it's not necessary to include SingleValue at all.
Now to the hard part which is the event handling.
Your interface declares that the onSelect prop is a function which receives an event of type React.FormEvent<HTMLSelectElement>. Unfortunately it is not possible to properly call this function because the "react-select" package doesn't pass the actual event to your callback functions.
If you want to keep using this package, you will need to change your SelectButtonProps interface to something which is compatible with the data that you are getting back from the "react-select" onChange function. For example, it can be a function which receives the selected Users object:
interface SelectButtonProps {
onSelect: (user: Users) => void;
users: Users[];
stateId: string;
}
The typescript type of the first argument given to onChange is any, but in practice it is the type of each option in your options array which is {label: string, value: (the type of your user.id which I don't know if it's string or number) }. Frankly, the package author could have done a better job there as they really should be giving you the correct type based on the options array that you are providing. There are multiple places that they should be using generics (value type, option type) where they are using vague types like any instead.
This is a function which you can pass to onChange:
const handleChange = (option: OptionTypeBase, meta: ActionMeta<any>): void => {
console.log({ option, meta });
onSelect({
name: option.label,
id: option.value
});
};
This is not necessary, but an alternative way of approaching this is that instead of mapping the users to a {label, value} and then mapping that {label, value} option back to a User in your onChange callback, what you can do is pass the users array directly to Select as options. Then instead of using the default behaviors for getOptionLabel (option => option.label) and getOptionValue (option => option.value), you can pass your own mappings, which are user => user.name and user => user.id.
Revised Code
I'm setting an array of movie objects in useEffect and want them sorted alphabetically. If possible later I want to add the functionality of sorting them by different properties but for now I just want to get over this TypeScript hump. I have a separate types file where I export my movie interface:
export interface Movie {
comments: string,
imdb: string,
mtc: string,
rtc: string,
smn: string,
title: string
}
Which I then import in my page
import { Movie } from '../types';
I call useEffect, request the list of movies from my MongoDB and then want to sort them.
const Database = () => {
const [movies, setMovies] = React.useState<Movie[]>([]);
const [sortType, setSortType] = React.useState('title');
const history = useHistory();
React.useEffect(() => {
Requests.getMovies()
.then(setMovies)
.catch (err => console.log(err));
const types: Movie = {
title: 'title',
rtc: 'rtc',
imdb: 'imdb',
smn: 'smn',
mtc: 'mtc',
comments: 'comments'
};
const sortArray = (sortProperty: string): sortProperty is keyof typeof types => {
const sorted = [...movies].sort((a: Movie, b: Movie) => b[sortProperty] - a[sortProperty] );
return sorted;
}
sortArray(sortType);
setMovies(sortArray);
}, [sortType])
return (
// Bunch of stuff on the page and then map out the movies like so..
<tbody>
{movies.map((movie, index) => (
<tr key={index} onClick={() => history.push(`/edit/${movie.title}`)}>
<td>{movie.title}</td>
<td>{movie.imdb}</td>
<td>{movie.rtc}</td>
<td>{movie.mtc}</td>
<td>{movie.smn}</td>
<td>{movie.comments}</td>
</tr>
))}
</tbody>
//Bunch of other stuff on the page..
)
}
export default Database;
I get the list of movies to show up no problem but sorting them is hard. I found this and this and this but TypeScript keeps giving me errors. On the return sorted...
Type 'Movie[]' is not assignable to type 'boolean'.
..and on the b[sortProperty] and a[sortProperty].
Element implicitly has an 'any' type because expression of type 'string | number | symbol' can't be used to index type 'Movie'.
No index signature with a parameter of type 'string' was found on type 'Movie'.
How do I get this to work? I thought sort returned the sorted array, but it returns a boolean? Why can't it sort by a property that is defined in the object and also in the interface?
ok here we go, i'll try to explain everything as best as i could.
first of all, you need to split your useEffect into 2 useEffect statement. useEffect hook should only handle one thing.
so your code would look like this:
React.useEffect(() => {
Requests.getMovies()
.then(setMovies)
.catch (err => console.log(err));
), []}
React.useEffect(() => {
// your sorting code
}, [/* dependencies */])
the reasoning behind this is, you don't want unneeded action to trigger for every change in your depencies, i.e. you don't want to fetch each time a dependencies change but only when your component mounts.
as far as the issues with your code, the errors have 2 reasons:
trying to do an arithmetic operation on 2 variables of type string
trying to read property of an object using variable of type string while your object has no generic keys
also do a change operation through a useEffect hook is not the most optimal solution, because you'll need the movies state in your dependencies so you get the latest state, but if you do you'll trigger an infinite loop.
the way i do it, is sort once on component mount using the fetched array and then use an event handler function to trigger to subsequent sorts. this way you would avoid any unintended side effects.
so your code would look like this:
const [movies, setMovies] = React.useState<Movie[]>([]);
const [sortType, setSortType] = React.useState<keyof Movie>("title");
React.useEffect(() => {
Requests.getMovies()
.then(sortMovies)
.catch (err => console.log(err));
), []}
const sortMovies = (moviesParam: Movie[] = movies) => {
const sortArray = [...moviesParam].sort((a: Movie, b: Movie) => {
if (a[sortType] > b[sortType]) return 1;
else if (b[sortType] > a[sortType]) return -1;
return 0;
});
setMovies(sortArray);
}
this code does sort your array alphabetically, and does not cause infinite loop.
also the sortArray function is not actually needed since you actually using the state anyway so it does not add any value to the code.
if you require further explanation on how and why i did those changes please add a comment and i'll update the post.
I have the following components:
const ParentComponent: React.FC = () => {
const networkRef: any = useRef();
// Somewhere in the code, I call this
networkRef.current.filter(["id0, id1, id2"]);
return (
...
<VisNetwork
ref={networkRef}
/>
...
)
}
export default ParentComponent;
interface Props {
ref: any;
}
const VisNetwork: React.FC<Props> = forwardRef((props: Props, ref) => {
useImperativeHandle(ref, () => ({
filter(items: any) {
setFilterNodes(items);
nView.refresh();
}
}));
const [filterNodes, setFilterNodes] = useState<any[]>([]);
const filterNodesRef = useRef(filterNodes);
useEffect(() => {
filterNodesRef.current = filterNodes;
}, [filterNodes]);
...
// Some code to create the network (concentrate on the nodesView filter method)
const [nView, setNView] = useState<DataView>();
const nodesView = new DataView(nodes, {
filter: (n: any) => {
if (filterNodesRef.current.includes(n.id)) {
return true;
}
return false;
}
})
setNView(nodesView);
const network = new vis.Network(container, {nodes: nodesView, edges: edgesView}, options);
});
export default VisNetwork;
WHen I call network.current.filter([...]), it will set the filterNodes state. Also, it should set the filterNodesRef inside the useEffect.
However, the filterNodesRef.current remains to be empty array.
But when I call network.current.filter([...]) the second time, only then the filterNodesRef.current got the value and the DataView was able to filter.
Why is it like this? I thought the useRef.current will always contain the latest value.
I finally solved this by calling the refresh() method inside the useEffect instead of the filter() method:
useEffect(() => {
filterNodesRef.current = filterNodes;
nView.refresh();
}, [filterNodes]);
Settings the .current of a reference does not notify the component about the changes. There must be some other reason why it works the second time.
From https://reactjs.org/docs/hooks-reference.html#useref
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
You may want to use useState, as this does rerender the component.
Two more things
I'm not really sure what networkRef.current.filter(["id0, id1, id2"]) is. Typescript does complain when I try to do ['a'].filter(['a']) and I've never seen this, so are you sure this is what you wanted to do?
If you're passing references around there's probably a better way to do it. Maybe consider re-thinking the relations between your components. Are you doing this because you need access to networkRef inside multiple components? If yes, you might want to look at providers.
If this does not answer your question, write a comment (about something specific please) and I'll be happy to try and help you with it :)
Yes, useRef.current contains latest value, but your filterNodesRef.current in a useEffect that's why you get empty array in initial render.
Initial render of VisNetwork the filterNodes is an empty array ==> filterNodesRef.current remains empty. Because setFilterNodes(items); is asyn function => event you set it in useImperativeHandle it will be updated in second render.
In useImperativeHandle you set setFilterNodes(items); ==> filterNodes is updated and the VisNetwork re-render ==> useEffect is triggered ==> filterNodesRef.current is set to new filterNodes
Let's try this:
....
const filterNodesRef = useRef(filterNodes);
useImperativeHandle(ref, () => ({
filter(items: any) {
filterNodesRef.current = filterNodes;
setFilterNodes(items);
nView.refresh();
}
}));
...
I have some components that are rendering another component (FetchNextPageButton) that is already tested in isolation, like these ones:
const News = () => (
<div>
<h1>News</h1>
...
<FetchNextPageButton query={NEWS_QUERY} path="viewer.news" />
</div>
)
const Jobs = () => (
<div>
<h1>Jobs</h1>
...
<FetchNextPageButton query={JOBS_QUERY} path="viewer.jobs" />
</div>
)
const Posts = () => (
<div>
<h1>Posts</h1>
...
<FetchNextPageButton query={POSTS_QUERY} path="viewer.posts" />
</div>
)
The thing is that I'd not like having to add tests on each of these components for a functionality that is already tested somewhere else, so I think that should be enough just to test that the component is rendered and that I'm passing the right props to it.
I'd have been able to test this easily with Enzyme with something like this:
expect(wrapper.find('FetchNextPageButton').props()).toMatchObject({
query: NEWS_QUERY,
path: "viewer.news"
})
So I'm wondering what's the best approach to test it by using React testing library instead.
This is the approach that Kent C. Dodds (the creator of RTL) shared with me after discussing it with him:
import FetchNextPageButton from 'FetchNextPageButton'
jest.mock('FetchNextPageButton', () => {
return jest.fn(() => null)
})
// ... in your test
expect(FetchNextPageButton).toHaveBeenCalledWith(props, context)
Don't believe it's possible. RTL looks like focusing on validating against DOM not React's components tree.
The only workaround I see is to mock FetchNextPageButton to make it rendering all props into attributes.
jest.mock("../../../FetchNextPageButton.js", () =>
(props) => <div data-test-id="FetchNextPageButton" {...props} />);
....
const { getByTestId } = render(<YourComponent />);
expect(getByTestId("FetchNextPageButton")).toHaveAttribute("query", NEWS_QUERY);
expect(getByTestId("FetchNextPageButton")).toHaveAttribute("path", "viewer.news");
Sure, this is smoothly only for primitive values in props, but validating something like object or function would be harder.
Think, it's not RTL-way, but I agree it would be massive work to check that in scope of each container(and completely ignoring that would be rather a risk).
PS toHaveAttribute is from jest-dom
In my case, I wanted to test that a Higher Order Component (HOC), correctly enhances the component that is passed to the HOC.
What I needed to do, is make the actual component a mock and pass it to the HOC. Like described in the existing answer, you can then just expect the properties, added by the HOC.
// after 'Component' get's passed into withSelectionConstraint, it should have an id prop
const Component = jest.fn(() => <h1>Tag Zam</h1>);
const WithConstraint = withSelectionConstraint(Component, ["instance"], true);
render(<WithConstraint />);
// passing the jest mock to the HOC, enables asserting the actual properties passed by the HOC
expect(Component).toHaveBeenCalledWith(
expect.objectContaining({ ids: mockInstanceRows.map(x => x.id) }),
expect.anything()
)
Based on Ben's answer, I wrote a version which doesn't raise any error :
jest.mock(
'path/to/your/component',
() => {
const MockedComponent = (props: any) => {
const cleanedProps = Object.keys(props).reduce<Record<string, unknown>>(
(a, b) => {
// Needed because html attributes cannot be camel cased
a[b.toLowerCase()] = props[b].toString();
return a;
},
{}
);
return (
<div data-testid="any-test-id" {...cleanedProps} />
);
};
return MockedComponent;
}
);
Note that the attributes values (expect(getByTestId('any-test-id')).toHaveAttribute('attribute','value')) will be stringified.