I took over a project from a coworker that does not longer work for us.
He has done things a bit different than I would have done it and now I'm pretty confused.
I have a parent component where I need to listen for changes made to the child and afterwards work with them in the parent component
Parent (I "obfuscated" props and fields due to company rules):
interface ParentProps {
prop1: string;
prop2?: string;
prop3?: boolean;
}
function handleChange(value: String) {
console.log(value);
}
export const Parent: FunctionComponent<ParentProps> = ({
prop1,
prop2,
prop3 = isTrue(prop1),
}) => {
return (
<Children
prop1Val={prop1}
prop2Val={prop2}
prop3Val={prop3}
maskSettings={{
mask: '##00 **** **** **** **** **** **** **** **',
definitions: {
'#': /[A-Z]/,
'*': /[A-Z0-9]/,
},
prepare: (input: string) => input.toUpperCase(),
}}
>
</Children>
);
};
export default Parent;
Children:
import { IMaskMixin } from 'react-imask';
import {
FormControl,
TextField,
TextFieldProps,
FormHelperText,
} from '#material-ui/core';
type ChildrenInputComponentProps = ChildrenProps | TextFieldProps;
const InputComponent: FunctionComponent<TextFieldProps> = (props) => (
<TextField {...props}/>
);
const ChildrenInputComponent: FunctionComponent<ChildrenInputComponentProps> = IMaskMixin(
InputComponent
);
interface ChildrenInputProps {
prop1: string;
prop2?: string;
prop3?: boolean;
maskSettings: MaskProps;
}
export const Children: FunctionComponent<ChildrenInputProps> = ({
prop1,
prop2,
prop3 = isTrue(prop1),
maskSettings,
}) => (
<div>
<ChildrenInputComponent
{...maskSettings}
unmask={true}
onAccept={(unmasked: string) =>
!!!!!!!!
use the handleChange from parent
!!!!!!!!
}
InputLabelProps={{
shrink: true,
}}
fullWidth
label={prop2}
required={prop3}
/>
</div>
);
export default Children;
How would I access the handleChange in this situation?
Thank you already!
You need to pass the handleChange from the parent to the child as props -
<Children onChange={handleChange} ... />
then call it in the child -
onAccept={(unmasked: string) =>
props.onChange(unmasked);
}
edit -
you'll need to add onChange to your props object -
export const Children: FunctionComponent<ChildrenInputProps> = ({
prop1,
prop2,
prop3 = isTrue(prop1),
maskSettings,
onChange, // <!--- here
}) => (
then call it like -
onAccept={(unmasked: string) =>
onChange(unmasked);
}
Related
I have the next component in my react application:
import "./styles.css";
type InpuType = "input" | "textarea";
interface IContainer {
name: string;
placeholder: string;
as: InpuType;
}
const Container = ({
name,
placeholder,
as,
...rest
}: IContainer &
(
| React.TextareaHTMLAttributes<HTMLTextAreaElement>
| React.InputHTMLAttributes<HTMLInputElement>
)) => {
const Comp = as || "input";
return <Comp name={name} placeholder={placeholder} {...rest} />;
};
export default function App() {
return (
<div className="App">
<Container name="hi" as="textarea" placeholder="start" />
</div>
);
}
The ...rest are all the default props that could be added for a textarea or input. I have a typescript issue here <Comp name={name} ..., hovering over the component i get this message:
Type '{ autoComplete?: string | undefined; autoFocus?: boolean | undefined; cols?: number | undefined; dirName?: string | undefined; disabled?: boolean | undefined; form?: string | undefined; ... 261 more ...; placeholder: string; } | { ...; }' is not assignable to type 'IntrinsicAttributes & ClassAttributes<HTMLInputElement> & InputHTMLAttributes<HTMLInputElement> & ClassAttributes<...> & TextareaHTMLAttributes<...>'.
and i am not sure how to get rid of that. Question: Why the issue appear and how to solve it? demo:https://codesandbox.io/s/react-typescript-forked-z6dk4i?file=/src/App.tsx:379-396
Instead of throwing all the native props using rest ({...rest}) maybe it would be better to just differentiate the props between textarea and input.
interface IContainer {
name: string;
placeholder: string;
textareaProps?: React.TextareaHTMLAttributes<HTMLTextAreaElement>;
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
}
const Container = ({
name,
placeholder,
textareaProps,
inputProps,
}: IContainer) => {
if (inputProps) {
return <input {...inputProps} name={name} placeholder={placeholder} />;
}
return <textarea {...textareaProps} name={name} placeholder={placeholder} />;
};
https://codesandbox.io/s/react-typescript-forked-j9kgh2?file=/src/App.tsx:51-523
you should create a function and create the inputs based on what you pass to it
import "./styles.css";
type InpuType = "input" | "textarea";
interface IComp {
name: string;
placeholder: string;
as: InpuType;
rest?: any;
}
const Comp = ({ as, name, placeholder, ...rest }: IComp) => {
switch (as) {
case "input":
return <input name={name} placeholder={placeholder} {...rest} />;
case "textarea":
return <textarea name={name} placeholder={placeholder} {...rest} />;
}
};
interface IContainer {
name: string;
placeholder: string;
as: InpuType;
}
const Container = ({
name,
placeholder,
as,
...rest
}: IContainer &
(
| React.TextareaHTMLAttributes<HTMLTextAreaElement>
| React.InputHTMLAttributes<HTMLInputElement>
)) => {
return <Comp as={as} name={name} placeholder={placeholder} {...rest} />;
};
export default function App() {
return (
<div className="App">
<Container name="hi" as="textarea" placeholder="start" />
</div>
);
}
Encountered a problem where I do not know how to work-around this in strongly typed variables.
So, I have a bunch of icons as components and an icon wrapper component.
Wrapper component has themeMode variable in order to determine in what color should icon be displayed. Hence, every icon needs themeMode variable, which is mandatory. However, I cannot think of the workaround when I have to pick an icon based of switch case scenario.
Here is the code for better understandment:
This component gets an id from a url and displays certain icon component.
However, every switch cases returning statements are underlined in red. That's because icon component NEEDS that themeMode variable. However, I'm giving it on iconWrapper component.
const CollectionEnd = () => {
const { id } = useParams();
const getEndSvg = () => {
switch (id) {
case "bags":
return <Bags />;
case "shoes":
return <Shoes />;
case "men":
case "women":
return <Clothes />;
case "hats":
return <Hats />;
default:
return <Clothes />;
}
};
return (
<Container>
<IconWrapper Component={getEndSvg()} />
</Container>
);
};
Icon wrapper component:
const IconWrapper: React.FC<{
Component: React.FC<{
themeMode: string;
style?: React.CSSProperties;
isOpened?: boolean;
}>;
isOpened?: boolean;
otherProps?: React.ReactNode;
style?: React.CSSProperties;
}> = ({ Component, style, isOpened, ...otherProps }) => {
const themeMode = useAppSelector(selectThemeMode);
return (
<div style={style}>
<Component {...otherProps} isOpened={isOpened} themeMode={themeMode} />
</div>
);
};
Here is one of the icon component's emitted code:
const Bags: React.FC<{ themeMode: string }> = ({ themeMode }) => {
return (
<svg
id='svg'
xmlns='http://www.w3.org/2000/svg'
width='200'
height='200'
viewBox='0, 0, 400,400'
version='1.1'
>
-----> ...emitedCode <------
Yes, I can simply make themeMode as not required, but that is not good. Also, I can wrap all switch case scenarios in IconWrapper. However, it looks repetitive.
One approach could be to change IconWrapper to accept React.ElementType and to pass the Icon component rather than an instantiated JSX.Element. This would let you delegate instantiating the Icon component to IconWrapper, which has the themeMode available.
In practice, this would look something like:
const CollectionEnd = () => {
const { id } = useParams();
const getEndSvg = () => {
switch (id) {
case "bags":
return Bags;
case "shoes":
return Shoes;
case "men":
case "women":
return Clothes;
case "hats":
return Hats;
default:
return Clothes;
}
};
return (
<Container>
<IconWrapper Component={getEndSvg()} />
</Container>
);
};
const IconWrapper: React.FC<{
Component: React.ElementType<{
themeMode: string;
style?: React.CSSProperties;
isOpened?: boolean;
}>;
isOpened?: boolean;
otherProps?: React.ReactNode;
style?: React.CSSProperties;
}> = ({ Component, style, isOpened, ...otherProps }) => {
const themeMode = useAppSelector(selectThemeMode);
return (
<div style={style}>
<Component {...otherProps} isOpened={isOpened} themeMode={themeMode} />
</div>
);
};
With React JS I can do this to pass function handleClick as a parameter from parent to child component:
Parent:
const testData = {
id: 1,
title: 'Tesco'
};
const Home = () => {
const handleClick = () => {
//do something
}
return (
<>
<Card id={testData.id} title={testData.title}, handleClick={handleClick}
</>
)
}
Child component:
const Card = ({id, title, handleClick}) => {
return (
<>
<div key={id}>
<p>{title}</p>
<button onClick={handleClick}>Click</button>
</div>
</>
)
}
Now I'm moving to TypeScript, I'm trying to do a similar thing with prop destructuring:
Parent: same as above
Interface:
interface IStores {
id: number;
title: string;
}
Child:
interface Props {
store: IStores
}
const Card = ({ store: {id, title} }: Props, handleClick: object) => {
return (
<>
<div key={id}>
<p>{title}</p>
<button onClick={handleClick}></button>
</div>
</>
)
}
No error in the child component, but in parent, on line <Card id={testData.id} title={testData.title}, handleClick={handleClick}, I got an error at handleClick:
Type '{ store: IStores; key: number; handleClick: () => void; }' is not assignable to type 'IntrinsicAttributes & Props'.
Property 'handleClick' does not exist on type 'IntrinsicAttributes & Props'.
Any idea how to pass handleClick to child in this case?
The typescript error is saying you don't have handleClick inside Props.
Explanation: You are basically saying, you will get object of type Props, which will have store, handleClick named key and its respective type of IStore, function
So, All you have to do is:
interface IStores {
id: number;
title: string;
}
interface Props {
store: IStores;
handleClick: () => void;
}
const Card = ({ store: {id, title}, handleClick }: Props) => {
return (
<>
<div key={id}>
<p>{title}</p>
<button onClick={handleClick}></button>
</div>
</>
)
}
For functional components, there's 2 parts to the arguments. There's the actual arguments themselves, then there's the typing of those arguments (separated by a colon). So this should work:
interface IStores {
id: number;
title: string;
}
interface Props {
store: IStores;
handleClick: () => void;
}
const Card = ({ store: {id, title} = {} as IStores, handleClick } : Props) => {
return (
<div key={id}>
<p>{title}</p>
<button onClick={handleClick}></button>
</div>
)
}
So here, { store: {id, title} = {} as IStores, handleClick } is the destructuring of the props, and Props is the typing of those destructured props, in this case defined in an interface
I have a simple compound component with a bunch of static subcomponents:
// #flow
import React, { Component, Children } from 'react';
type Props = {
children: React.ChildrenArray<React.Node> | React.Node,
}
class Toggle extends Component<Props> {
static On = props => (props.on ? props.children : null);
static Off = props => (props.on ? null : props.children);
static Button = props => (
<button
onClick={props.toggle}
type="button"
style={{ display: 'inline-block' }}
>
<pre>{JSON.stringify(props.on, null, 2)}</pre>
</button>
);
state = { on: false }
toggle = () => {
this.setState(
({ on }) => ({ on: !on }),
// maybe this.props.someCallback
() => console.log(this.state.on),
);
}
render() {
return Children.map(
this.props.children,
childElem => React.cloneElement(childElem, {
on: this.state.on,
toggle: this.toggle,
}),
);
}
}
export default Toggle;
The warning happens when I try to put some other elements into Toggle children scope.
For example:
<Toggle>
<Toggle.On>On</Toggle.On>
<span /> <-- this is savage
<Toggle.Button />
<Toggle.Off>Off</Toggle.Off>
</Toggle>
Everything is working, but my flowtype warn me about this span like so:
Warning: Received `false` for a non-boolean attribute `on`.....
Warning: Invalid value for prop `toggle` on <span> tag....
How can I to pacify this nasty girl?
Thank you guys, I think, right solution is just check if type of mounted node is correct one, otherwise - just clone node with regular node props:
// #flow
import React, { Component, Children } from 'react';
type Props = {
children: React.ChildrenArray<React.Node> | React.Node,
}
class Toggle extends Component<Props> {
static On = props => (props.on ? props.children : null);
static Off = props => (props.on ? null : props.children);
static Button = props => (
<button
onClick={props.toggle}
type="button"
style={{ display: 'inline-block' }}
>
<pre>{JSON.stringify(props.on, null, 2)}</pre>
</button>
);
state = { on: false }
toggle = () => {
this.setState(
({ on }) => ({ on: !on }),
// maybe this.props.someCallback
() => console.log(this.state.on),
);
}
// Checking here
allowedTypes = ({ type }) => {
return [
(<Toggle.On />).type,
(<Toggle.Off />).type,
(<Toggle.Button />).type,
].includes(type);
}
render() {
return Children.map(
this.props.children,
(childElem) => {
const elemProps = this.allowedTypes(childElem) ? {
on: this.state.on,
toggle: this.toggle,
} : childElem.props;
return React.cloneElement(childElem, elemProps);
},
);
}
}
export default Toggle;
You can also do this, just having the components in a list and checking their type inside .map, putting on the custom props or otherwise just returning the original child.
const allowedTypes = [ToggleOn, ToggleOff, ToggleButton]
return React.Children.map(props.children, child => {
if (allowedTypes.includes(child.type)) {
return React.cloneElement(child, {on, toggle})
}
return child
})
}
I'm trying to copy the example of React+Redux component to typescript: https://medium.com/#stowball/a-dummys-guide-to-redux-and-thunk-in-react-d8904a7005d3
I'm hitting a deadend with the function:
export default connect(mapStateToProps, mapDispatchToProps)(ItemList);
I'm getting an error about how it's not mappable to ItemList.
One way around this I believe is to change the class declaration to:
class ItemList extends React.Component<{proper component map}, {proper state map}> {
However if I do this because the props have now been mapped I cannot simply include ItemList as and am now expected to provide the params.
Another option might be to: (props as any).fetchData() however this feels wrong.
Is there a way around this? Am I doing React+Redux wrong in typescript?
After you create everything, you export it together with connect.
interface PassedProps {
productId: number;
}
interface StateToProps {
addedProductIds: number[];
quantityById: { [key: string]: number };
quantity: number;
}
interface DispatchToProps {
addToCart: (productId: number) => void;
removeFromCart: (productId: number) => void;
}
// Needs to be added to src/store:GlobalStore interface with the correct prop name created from the name of the reducer
export interface CartState {
addedProductIds: number[];
quantityById: { [key: string]: number };
}
const mapStateToProps = (globalState: GlobalState): StateToProps => {
const state: CartState = globalState.cart;
return {
addedProductIds: state.addedProductIds,
quantityById: state.quantityById,
quantity: Object.keys(state.quantityById).reduce( (sum: number, key: string) => state.quantityById[key] + sum, 0)
};
};
const mapDispatchToProps = (dispatch: Dispatch<any>): DispatchToProps => {
return {
addToCart: (productId: number) => dispatch({ type: 'ADD_TO_CART', productId } as AddToCartAction),
removeFromCart: (productId: number) => dispatch({ type: 'REMOVE_FROM_CART', productId } as RemoveFromCartAction),
};
}
export type Props = PassedProps & StateToProps & DispatchToProps;
class CartButton extends Component<Props, CartState> {
render() {
const { quantity } = this.props;
return (
<View>
<Text>
{ this.props.addedProductIds.length } type item is in the cart, totals to { quantity } item.
</Text>
<View>
<Button
onPress={this.onPressAdd.bind(this)}
title="Add to Cart"
color="#841584"
/>
<Button
onPress={this.onPressRemove.bind(this)}
title="Removefrom Cart"
color="#841584"
/>
</View>
</View>
);
}
onPressAdd() {
this.props.addToCart(this.props.productId);
}
onPressRemove() {
this.props.removeFromCart(this.props.productId);
}
}
export default connect<StateToProps, DispatchToProps, PassedProps>(mapStateToProps, mapDispatchToProps)(CartButton);
Then you can use it with specifying with the required props to be passed (PassedProps interface):
import CartButton from '../components/CartButton';
// ....
render() {
return(<CartButton productId={5} />)
}