Converting React Component to Typescript? - javascript

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} />)
}

Related

TypeScript: how to add a function as a parameter along with prop destructuring with React function component?

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

React use onChange from parent

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);
}

Updating array in react state, using concat

I have a component of checkbox, basically what im trying to do is to make array of the checked ones and send the information to an api.
but im having trouble updating the state array without deleteing the last object inserted.
im using concat and still same result.
here is the code for the whole component:
import React, { Fragment, Component } from 'react';
import { Inputt, Checkbox } from './style';
interface Istate {
checked: boolean;
products?: any[];
}
interface Iprops {
id: any;
value: any;
changeValue?: (checkBoxId: string, value: any) => void;
}
class CheckBoxComp extends Component<Iprops, Istate> {
state = {
checked: true,
products: [] as any,
};
addOne = (id: any, checked: any) => {
let { products } = this.state;
const newObj = { id, checked };
products = products.concat(newObj);
this.setState({ products }, () => {
console.log(products);
});
};
isChecked = (id: any) => {
const { checked, products } = this.state;
const { changeValue } = this.props;
this.setState({
checked: !checked,
});
if (changeValue) {
changeValue(id, checked);
}
this.addOne(id, checked);
};
render() {
const { id, value } = this.props;
const { checked, products } = this.state;
return (
<Fragment>
<Checkbox>
<span />{' '}
<label className="checkbox-wrapper">
<span className="display" />
<Inputt
type="checkbox"
value={checked}
onChange={() => {
this.isChecked(id);
}}
/>
<span className="checkmark" />
</label>
</Checkbox>
</Fragment>
);
}
}
export default CheckBoxComp;
You can try this approach which utilizes the spread operator to make an array copy:
addOne = (id: any, checked: any) => {
this.setState((state: Istate): Partial<Istate> => ({
products: [
...state.products,
{ id, checked },
],
}), () => console.log(this.state.products));
};

How to remove data from API with HTTP Request [React TS]

so I've been working on an admin control panel for a page that displays a list of cards that once clicked, redirect the user to either a video ora text-based link. I must also add a CRUD feature that allows me to edit / remove posts from the API provided.
The API is locally hosted, so requesting "localhost:3000/videos/{Id}" will load up the object with the following fields: "id", "url", "title" and "thumbnail"
I have a class that is used for making the card, called HelpCard.tsx The code is as follows:
import React, { Component } from "react";
import "../help/HelpCard.css";
import "../help/HelpList";
import spinner from "../help/spinner.gif";
import { string, number } from "prop-types";
import { Link } from "react-router-dom";
interface Props {
url: string;
title: string;
thumbnail: string;
}
interface State {
title: string;
thumbnail: string;
id: string;
imageLoading?: boolean;
tooManyRequests?: boolean;
}
export default class HelpCard extends Component<Props, State> {
state = {
id: "",
title: "",
imageLoading: true,
tooManyRequests: false,
thumbnail: "",
deleteProduct: true
};
componentDidMount() {
const { url, title, thumbnail } = this.props;
const id = url.split("/")[url.split("/").length - 2];
this.setState({
id,
title,
thumbnail,
imageLoading: true,
tooManyRequests: false
});
}
render() {
const isThumbnail = this.state.thumbnail;
const adminhelpcard = this.state;
return (
<div>
{isThumbnail ? (
<div className="horizontalCard">
<div className="innerCard">
<div className="leftImage">
<img
className="Sprite"
onLoad={() => this.setState({ imageLoading: false })}
onError={() => this.setState({ tooManyRequests: true })}
src={this.state.thumbnail}
style={
this.state.tooManyRequests
? { display: "none" }
: this.state.imageLoading
? { display: "null" }
: { display: "null" }
}
/>
</div>
<div className="rightText">
<div className="card-body">
{this.state.title}
<div className="cardButtons">
<button className="btn btn-update btn-outline-primary">Update</button>
<button
onClick={() => adminhelpcard.deleteProduct(this.state.id)}
className="btn btn-outline-primary">
Delete
</button>
</div>
</div>
</div>
</div>
</div>
And then I have the HelpList.tsx module that is responsible for displaying the cards in the form of a list. and the code is as follows:
import React, { Component } from "react";
import HelpCard from "./HelpCard";
import "../help/HelpCard.css";
import axios from "axios";
import InfiniteScroll from "react-infinite-scroller";
import { Button } from "components/ui";
interface State {
url: string;
adminhelpcard: SingleAdminHelpCard[];
error: null;
response: {};
}
interface SingleAdminHelpCard {
id: string;
url: string;
title: string;
thumbnail: string;
}
interface Props {}
export default class HelpList extends Component<Props, State> {
state = {
id: "",
url: "http://localhost:3000/videos/",
adminhelpcard: [],
itemsCountPerPage: 1,
activePage: 1,
error: null,
response: {}
};
loadAdminHelpCard = () => {
axios
.get(this.state.url)
.then((res) => {
this.setState((prevState) => {
const adminhelpcard = prevState.adminhelpcard;
return {
adminhelpcard: [...prevState.adminhelpcard, ...res.data],
url: res.data.next
};
});
})
.catch(function(error) {
// handle error
console.log(error);
});
};
async componentDidMount() {
const apiUrl = "http://localhost:3000/videos/";
const res = await axios.get(this.state.url);
this.setState({ adminhelpcard: res.data });
fetch(apiUrl)
.then((res) => res.json())
.then(
(result) => {
this.setState({
adminhelpcard: result
});
},
(error) => {
this.setState({ error });
}
);
}
deleteProduct(id: any) {
const { adminhelpcard } = this.state;
const apiUrl = `http://localhost:3000/videos/${this.state.id}`;
const options = {
method: "DELETE"
};
fetch(apiUrl, options)
.then((res) => res.json())
.then(
(result) => {
this.setState({
response: result,
adminhelpcard: adminhelpcard.filter((adminhelpcard: SingleAdminHelpCard) => adminhelpcard.id !== id)
});
},
(error) => {
this.setState({ error });
}
);
}
render() {
console.log(this.state.adminhelpcard);
return (
<div>
<React.Fragment>
{this.state.adminhelpcard ? (
<div className="row">
<InfiniteScroll
pageStart={1}
loadMore={this.loadAdminHelpCard}
hasMore={this.state.url ? true : false}
threshold={0}
loader={
<div className="loader" key={0}>
Loading ...
</div>
}>
{this.state.adminhelpcard.map((adminhelpcard: SingleAdminHelpCard, i) => (
<HelpCard
key={adminhelpcard.id + i}
title={adminhelpcard.title}
url={adminhelpcard.url}
thumbnail={adminhelpcard.thumbnail}
/>
))}
</InfiniteScroll>
</div>
) : (
<h1>Loading Cards</h1>
)}
</React.Fragment>
</div>
);
}
}
I get the following error:
This expression is not callable.
Type 'Boolean' has no call signatures.ts(2349)
from the "deleteProduct" function in the line of code:
onClick={() => adminhelpcard.deleteProduct(this.state.id)}
When I try to click the delete button on the helpCards, it says that the no function called "deleteProduct" is recognized. How do I go about fixing this?
--------------------------EDIT----------------------------
Error given for adding deleteProduct to HelpCard component.
"No overload matches this call.
Overload 1 of 2, '(props: Readonly): HelpCard', gave the following error.
Type '{ key: string; title: string; url: string; thumbnail: string; deleteProduct: (id: any) => void; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly & Readonly<{ children?: ReactNode; }>'.
Property 'deleteProduct' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly & Readonly<{ children?: ReactNode; }>'."
First, you need to pass the deleteProduct function as a prop to the HelpCard component.
So in your HelpList.tsx, add another prop to the <HelpCard/> element as follows.
<HelpCard
key={adminhelpcard.id + i}
title={adminhelpcard.title}
url={adminhelpcard.url}
thumbnail={adminhelpcard.thumbnail}
deleteProduct={this.deleteProduct.bind(this)}
/>
Then you need to use it from the onClick handler within the HelpCard component.
<button
onClick={() => this.props.deleteProduct(this.state.id)}
className="btn btn-outline-primary">
Delete
</button>
Probably you may need to change the Props interface in your HelpCard to cater to this new prop as well.
interface Props {
url: string;
title: string;
thumbnail: string;
deleteProduct: (id: any) => void;
}

ReactJS | Cannot update during an existing state transition

I am trying to add Delete Functionality, to my React Application. So, I created a Delete Model Component. And I am using it in my main page.
Main-Page Component:
import IUser from '../../dto/IUser';
import DeleteUser from '../../components/DeleteUser';
import { listUsers, getUser, deleteUser } from '../../config/service';
interface UserDetailsProps extends RouteComponentProps<RouteUserInfo> {
notify(options: object): any;
actualValue: string;
callBack: any;
label: string;
}
interface RouteUserInfo {
username: string;
}
export interface IState {
errorMessage: LensesHttpResponseObj | null;
isUserDeleteModalOpen: boolean;
isLoading: boolean;
user: IUser | null;
}
const UserToolTip = (props: any): JSX.Element => (
<LensesTooltip id="isActive" place="right" {...props} />
);
export class UserDetailsPage extends Component<UserDetailsProps, IState> {
hasBeenMounted = false;
state: IState = {
isUserDeleteModalOpen: false,
errorMessage: null,
isLoading: false,
user: null
};
componentDidMount(): any {
this.hasBeenMounted = true;
this.onFetchData();
}
componentWillUnmount(): void {
this.hasBeenMounted = false;
}
getUserUsername = (): string => {
const { match } = this.props;
return match.params.username;
};
onFetchData = () => {
this.setState({
isLoading: true
});
return this.onFetchUser();
};
onFetchUser = () =>
getUser(this.getUserUsername())
.then(username => {
if (this.hasBeenMounted && typeof username !== 'undefined') {
this.setState({
isLoading: false,
user: username.data
});
}
})
.catch((errorResponse: HttpResponseObj | null = null) => {
if (this.hasBeenMounted) {
this.setState({
isLoading: false,
user: null,
errorMessage: errorResponse
});
}
});
openUserDeleteModal = () => {
this.setState(prevState => ({
...prevState,
isUserDeleteModalOpen: true
}));
};
closeUserDeleteModal = () => {
this.setState(prevState => ({
...prevState,
isUserDeleteModalOpen: false
}));
};
// Dropdown Render Method:
<Item onClick={this.openUserDeleteModal()}> // The error appears when I add the onClik
<Icon icon="trash-o" className=" pl-0 py-2 col-1" />
<span className="col pr-0 mr-0">Delete User</span>
</Item>
Of course I call the dropdown render Method inside the main render(), along with the render method for the Delete Component:
renderUserDeleteModal = (): JSX.Element | null | void => {
const { isUserDeleteModalOpen, user } = this.state;
if (!user || !user.username) {
return null;
}
return (
<DeleteUser
isModalOpen={isUserDeleteModalOpen}
user={user}
onSuccess={this.closeDeleteModalSuccess}
onCloseModal={this.closeUserDeleteModal}
/>
);
};
But I get this ERROR: warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state.
I am not sure what am I doing wrong here. To me, it seems legit. Can you explain what am I doing wrong. Thank you!!
you are making call to openUserDeleteModal onClick={this.openUserDeleteModal()} which is causing update of state while rendering the component try the following :
<Item onClick={this.openUserDeleteModal}>
onClik
<Icon icon="trash-o" className=" pl-0 py-2 col-1" />
<span className="col pr-0 mr-0">Delete User</span>
</Item>
You need not invoke the callback to your onClick as that will end up being immediately called upon render.
Remove the parenthesis following onClick={openUserDelete()}.
Your openUserDelete is being called straight away (upon render) and changes the state Object.
changing the state causes a re-render and you can imagine how this would get out of hand...
render > change > render > change...etc

Categories