Can someone help me? I'm creating a component inside React, and I want to make it more accessible using forwardRef. In my case, I'm making a button and I'm using the button's properties, and a few more I've done to make it more dynamic.
This is a summary of my code.
export interface ButtonProps
extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
children?: React.ReactNode;
loading?: boolean;
}
class Button extends React.Component<ButtonProps> {
render() {
const {
...otherProps
} = this.props;
return (
<button {...(otherProps)}></button>
)
}
}
export default Button;
I tried to start something, but right away it gave an error
const ForwardedElement = React.forwardRef<ButtonProps, HTMLButtonElement> (
(props: ButtonProps, ref) => <Button {...props}/>
)
export default ForwardedElement;
I suggest you to use useImperativeHandle hook
useImperativeHandle customizes the instance value that is exposed to parent components when using ref. Let's visualize that with an example.
Here's a component as search bar
import React, {forwardRef, useImperativeHandle, useRef} from "react";
import {Form} from "reactstrap";
const SearchBar = (props, ref) => {
const buttonRef = useRef<any>();
useImperativeHandle(ref, () => ({
getCurrentValue: () => {
return buttonRef.current ? buttonRef.current["value"] : '';
},
setCurrentValue: (value) => {
if (buttonRef.current) {
buttonRef.current["value"] = value;
}
}
}));
return (
<Form className="p-3 w-100" onSubmit={(e) => props.onSubmitHandler(e)}>
<div className="form-group m-0">
<div className="input-group">
<input
type="text"
className="form-control"
placeholder="Search ..."
aria-label="Word to be searched"
ref={buttonRef}
/>
<div className="input-group-append">
<button className="btn btn-primary" type="submit">
<i className="mdi mdi-magnify" />
</button>
</div>
</div>
</div>
</Form>
);
}
export default forwardRef(SearchBar);
This is the header component in which we call our search bar component
import React, {useEffect, useRef, useState} from 'react';
import SearchBar from '../Form/Search/SearchBar';
import Router from 'next/router';
const Header = () => {
const mobileSearchRef = useRef<any>();
const [search, setSearch] = useState<any>(false);
const codeSearchHandler = (e) => {
e.preventDefault();
setSearch(!search);
if (mobileSearchRef.current) {
if (mobileSearchRef.current.getCurrentValue() == '') {
return;
}
}
Router.push({
pathname: '/search',
query: {
searchTerm: mobileSearchRef.current
? mobileSearchRef.current.getCurrentValue()
: ''
},
});
mobileSearchRef.current.setCurrentValue('');
};
return (
<React.Fragment>
<header id="page-topbar">
<div className="navbar-header">
<div className="d-flex">
<div className="dropdown d-inline-block d-lg-none ms-2">
<button
onClick={() => {
setSearch(!search);
}}
type="button"
className="btn header-item noti-icon mt-2"
id="page-header-search-dropdown"
>
<i className="mdi mdi-magnify" />
</button>
<div
className={
search
? 'dropdown-menu dropdown-menu-lg dropdown-menu-end p-0 show'
: 'dropdown-menu dropdown-menu-lg dropdown-menu-end p-0'
}
aria-labelledby="page-header-search-dropdown"
>
<SearchBar
id="headerSearchBar"
ref={mobileSearchRef}
onSubmitHandler={(e) => codeSearchHandler(e)}
/>
</div>
</div>
</div>
</div>
</header>
</React.Fragment>
);
};
export default Header;
If we look at the header component, we can see that we get the input value of search bar component using mobileSearchRef and getCurrentValue method. We can also set its value using setCurrentValue method.
You have to pass the ref aside the spread props:
export interface ButtonProps
extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
children?: React.ReactNode;
loading?: boolean;
ref?: React.RefObject<HTMLButtonElement>
}
class Button extends React.Component<ButtonProps> {
render() {
const {
...otherProps,
ref
} = this.props;
return (
<button {...otherProps} ref={ref}></button>
)
}
}
export default Button;
const ForwardedElement = React.forwardRef<ButtonProps, HTMLButtonElement> (
(props: ButtonProps, ref) => <Button {...props} ref={ref}/>
)
export default ForwardedElement;
now it should work, see this question
Related
I am trying to create a reusable carousel, where carousel items are scrollable and clickable. Carousel item can be any component which is passed as children to Carousel. The problem is that when I try to pass selected state as prop in the child component using React.cloneElement I get this error react docs Unknown prop warning. There are several answers in stack overflow like this that inspired my solutionand this that exposes the problembut with no good answers specific to my situation. My code so far is this:
import React, { useState } from 'react';
import CarouselItem from './CarouselItem';
type Props = {
children: Array<React.FC>;
};
const Carousel: React.FC<Props> = ({ children }: Props) => {
const [selected, setSelected] = useState(0);
const handleSelect: (index: number) => void = (index: number) => {
setSelected(index);
};
console.log('children', children);
return (
<div className="flex w-full items-center justify-start overflow-y-scroll">
{children.map((child, index) => (
<CarouselItem
key={`carousel-item${index + 1}`}
selected={selected}
index={index}
child={child}
handleSelect={handleSelect}
/>
))}
</div>
);
};
export default Carousel;
import Link from 'next/link';
import React, { useState, useEffect, createContext } from 'react';
interface Props {
selected: number;
index: number;
child: JSX.Element;
handleSelect: (index: number) => void;
}
const CarouselItem: React.FC<Props> = (props) => {
const a = 1;
// props.child.props.children.props.isSelected = 'true';
console.log('typeof props.child', typeof props.child.type);
return (
<li
onClick={() => props.handleSelect(props.index)}
className={`${
props.selected == props.index ? 'selected' : 'unselected'
} list-none`}
>
{React.cloneElement(props.child, {
isSelected: 'true'
})}
</li>
);
};
export default CarouselItem;
const LiveSport: NextPage = () => {
console.log();
return (
<>
<div className="hidden">
<SportsSvgIcons />
</div>
<Carousel>
{carouselMockData.map((itemData, index) => (
<div key={`sport-carousel-${index + 1}`}>
<CarouselSportItem {...itemData} />
</div>
))}
</Carousel>
</>
);
};
export default LiveSport;
The error comes from here:
const CarouselSportItem: React.FC<CarouselSportItemData> = (props) => {
console.log('props', props);
const divProps = Object.assign({}, props);
delete divProps.isSelected;
return (
<div
className={``}
>
<Link href={`${props.sportCode}`}>
<div>
<div>
<div>{`${props.name}`}</div>
</div>
<div>
<div>
{props.isSelected}
</div>
</div>
</div>
</Link>
</div>
);
};
export default CarouselSportItem;
props.isSelected is not there, and I get this error message:
Warning: React does not recognize the isSelected prop on a DOM
element. If you intentionally want it to appear in the DOM as a custom
attribute, spell it as lowercase isselected instead. If you
accidentally passed it from a parent component, remove it from the DOM
element.
Im trying to solve this for 2 days but cant.
How to call onSubmit method in CreateProject component when onApply function called in ModalContent component with Typescript and react-hook-form.
The idea: when I clicked button in ModalContent it should call react-hook-form onSumbit method in CreateProject. Then CreateProject calls onSubmit in parent component of these two childs.
Or maybe the idea of such component structure is wrong?
Thanks everyone for any answers.
Parent component:
import { ModalBody } from '../../components/modals'
import useCreateProject from '../../api/hooks/createProject'
export default function New(): JSX.Element {
const [modalVisible, setModalVisible] = React.useState(true)
const onSubmit = useCreateProject((data) => {
const { createProject } = data.data.data
console.log(data)
})
const onApply = () => {
console.log(123)
}
return (
<ModalContent
name={'Create project'}
visible={modalVisible}
onApply={onApply}
onCancel={() => setModalVisible(false)}
>
<ModalBody>
<CreateProject onSubmit={onSubmit.mutate} />
</ModalBody>
</ModalContent>
)
}
Child component
import React from 'react'
import Input from '../Inputs/Input'
import Textarea from '../Inputs/Textarea'
import FileUploader from '../uploader/FileUploader'
import Tasks from '../forms/Tasks'
import { useForm } from 'react-hook-form'
import {
draftBudgetMaxLength,
projectNameMaxLength,
descriptionMaxLength,
} from '../../constants/createProjectModal'
import ButtonSecondary from '../buttons/ButtonSecondary'
export interface IModalInputs {
create_project_name: string
}
export interface ICreateProjectProps {
onSubmit: (values: IModalInputs) => void
}
const СreateProject: React.FC<ICreateProjectProps> = ({
onSubmit,
}): React.ReactElement => {
const {
register,
getValues,
control,
handleSubmit,
formState: { errors },
} = useForm<IModalInputs>()
return (
<>
<form onSubmit={handleSubmit(() => onSubmit(getValues()))}>
<div className="p-4">
<div className="flex flex-col -m-1.5">
<div className="m-1.5">
<Input
type="text"
label="Project name"
name="create_project_name"
register={register}
error={errors.create_project_name}
options={{
required: true,
maxLength: projectNameMaxLength,
}}
/>
</div>
</div>
</div>
</form>
</>
)
}
export default СreateProject
Child component with click event
import React from 'react'
import Image from 'next/image'
import close from '../../assets/close.svg'
import ButtonSecondary from '../buttons/ButtonSecondary'
interface IModalContentProps {
children: React.ReactElement
onApply?: () => void
visible: boolean
buttonName?: string
}
const ModalContent: React.FC<IModalContentProps> = ({
name,
children,
visible,
onCancel,
onApply,
buttonName,
}) => {
return (
<>
{visible && (
<div className="flex p-4 space-x-2 justify-end">
{children}
<ButtonSecondary
click={onApply}
type={'submit'}
label={buttonName || 'Ok'}
id={buttonName}
shown={true}
styleClass="styleClass"
paddingClass="py-2 py-2 pr-4 pl-2"
/>
</div>
)}
</>
)
}
export default ModalContent
The problem was resolved with useRef and useImperativeHandle hooks.
Parent:
export default function New(): JSX.Element {
const [modalVisible, setModalVisible] = React.useState(true)
const childRef = React.useRef<any>()
const onSubmit = useCreateProject((data) => {
const { createProject } = data.data.data
console.log(data)
})
return (
<ModalContent
name={'Create project'}
visible={modalVisible}
onApply={() => childRef.current.SubmitForm()}
onCancel={() => setModalVisible(false)}
>
<ModalBody>
<CreateProject onSubmit={onSubmit.mutate} ref={childRef} />
</ModalBody>
</ModalContent>
)
}
Child:
export interface IModalInputs {
create_project_name: string
}
export interface ICreateProjectProps {
onSubmit: (values: IModalInputs) => void
}
function CreateProject(props: ICreateProjectProps, ref) {
const {
register,
getValues,
control,
handleSubmit,
formState: { errors },
} = useForm<IModalInputs>()
useImperativeHandle(ref, () => ({
SubmitForm() {
handleSubmit(() => props.onSubmit(getValues()))()
},
}))
return (
<>
<form ref={ref}>
<div className="p-4">
<div className="flex flex-col -m-1.5">
<div className="m-1.5">
<Input
type="text"
label="Project name"
name="create_project_name"
register={register}
error={errors.create_project_name}
options={{
required: true,
maxLength: projectNameMaxLength,
}}
/>
</div>
</div>
</div>
</form>
</>
)
}
export default React.forwardRef(CreateProject)
I am new to React and I am trying to filter a list of emails in .users-list. I just want to return what the user is typing on the SearchBox but it does not work. Any suggestions?
Dashboard.js
import React, { Component } from "react";
import Button from "../Button/Button";
import SearchBox from "../SearchBox/SearchBox";
import "./Dashboard.css";
import fire from "../../fire"
class Dashboard extends Component {
constructor(){
super();
this.state = {
users:[],
searchField:''
}
}
handleLogout = () => {
fire.auth().signOut();
};
render() {
const {users, searchField} = this.state
const filteredUsers = users.filter(users => (users.users.toLowerCase.inc))
return (
<div>
<h2>Welcome</h2>
<button onClick={this.handleLogout}>Logout</button>
<div className="users-container">
<div>
<SearchBox
placeholder="Enter email..."
handleChange={(e) =>
this.setState({ searchField: e.target.value})
}
/>
</div>
<ul className="users-list">
<li>
<span>jean#gmail.com</span>
</li>
<li>
<span>albert#gmail.com</span>
</li>
<li>
<span>kevin#gmail.com</span>
</li>
<li>
<span>lucie#gmail.com</span>
</li>
</ul>
</div>
</div>
);
}
}
export default Dashboard;
SearchBox.js
import React from 'react';
const SearchBox = (props) => {
return(
<input
type='search'
className='search'
placeholder={props.placeholder}
onChange={props.handleChange}
/>
)
}
export default SearchBox
You can follow: codesandbox DEMO
For optimize performance:
useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.
import React, { useMemo, useState } from "react";
import "./styles.css";
const usersData = [
{id:1,email: 'jean#gmail.com'},
{id:2,email: 'albert#gmail.com'},
{id:3,email: 'kevin#gmail.com'},
]
export default function App() {
const [search, setSearch] = useState("");
const filteredUsers = useMemo(() => {
if (search) {
return usersData.filter(
(item) =>
item.email
.toLowerCase()
.indexOf(search.toLocaleLowerCase()) > -1
);
}
return usersData;
}, [search]);
return (
<div className="App">
<h1>users list</h1>
<input type="search" name="search" value={search} onChange={e => setSearch(e.target.value)} />
<ul>
{filteredUsers.length > 0 ?
(filteredUsers && filteredUsers.map(item => (
<li key={item.id}>{item.email}</li>
))): <div>empty</div>
}
</ul>
</div>
);
}
Modal.tsx
import React from 'react';
import './Modal.css'
interface IProps {
show?: boolean;
handleClose?: any;
children: any;
};
const Modal:React.SFC<IProps> = ({ handleClose, show, children }: IProps) => {
const showHideClassName = show ? "display-block" : "display-none";
return <div className={showHideClassName}>
<section className="modal-main">
{children}
<button onClick={handleClose}>close</button>
</section>
</div>;
};
export default Modal;
infoPresenter.tsx
import { Icon } from 'antd';
import React from 'react';
import Modal from '../Modal';
import './Info.css';
class Info extends React.Component{
public state = {
show: false
};
public showModal = () => {
this.setState({ show:true })
}
public hideModal = () => {
this.setState({ show:false })
}
public render(){
return (
<section className="Info">
<div className="InfoTitle">
<h1>Philociphy</h1>
<p>
lorem ipsum
</p>
</div>
<div className="WholeBox">
<div className="BoxLeft" onClick={this.showModal}>
<Modal show={this.state.show} handleClose={this.hideModal}>
<p>Modal</p>
<p>Data</p>
</Modal>
<p>VISION</p>
<Icon type="arrow-left" />
</div>
<div className="BoxRight">
<p>VALUE</p>
<Icon type="arrow-right" />
</div>
</div>
</section>
)
}
}
export default Info;
I want to make the modal. it works but not vanish.
I checked all my state and props, but couldn't find problems.
I don't know where's wrong in my code.
please find it and fix it.
if you have more good idea which make the modal, please let me know how make it in typescript react.
Thanks for all guys who watch this questions
I am working on the below snippet, and I'd like to find out why I am not able to bind onToggle event with button component. I am getting 'onToggle' is not defined on compiling.
In the main container (Content) I have:
class Content extends Component {
constructor() {
super();
this.state = {
user: dataService.User
}
}
onTogglePane(){
var node = ReactDOM.findDOMNode(this.refs.wrapper);
node.classList.toggle('toggled');
}
onSignOut() {
dataService.Logout((result) => {
this.setState({
user: null
})
});
}
render() {
return (
<div>
<Header
onClick = {this.onSignOut}
onToggle ={this.onTogglePane}
/>
</div>
)
}
}
In the Button.js I have button component as:
import React from 'react';
const Button = ({ text, styleClass, onClick }) => {
return (
<button
type="button"
onClick={e => onClick(e)}
onToggle={e => onToggle(e)}
className={`btn ${styleClass}`}
>
{text}
</button>
);
};
export default Button;
and finally in the Header.js I have
import React from 'react';
import Button from 'components/Button';
class Header extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<nav className="navbar navbar-default hk-navbar fixed-top">
<p className="navbar-brand tk-brand">App</p>
<Button
text={[<i class="icon icon-logout"></i>, " Sign Out"]}
onClick = {(e) => this.props.onClick(e)}
styleClass = 'btn-control'
/>
<Button
text={[<i class="icon icon-logout"></i>, " Full Screen"]}
onToggle = {(e) => this.props.onToggle(e)}
styleClass = 'btn-control'
/>
</nav>
);
}
}
export default Header;
Now I am getting this error:
Failed to compile
./src/components/Button.js
Line 8: 'onToggle' is not defined no-undef
Search for the keywords to learn more about each error.
Try this, you are missing onToggle which you are passing it to Button component.
Below Button component code would fix the issue
import React from 'react';
const Button = ({ text, styleClass, onClick, onToggle }) => {
return (
<button
type="button"
onClick={onClick ? onClick: null}
onToggle={onToggle ? onToggle: null}
className={`btn ${styleClass}`}
>
{text}
</button>
);
};
export default Button;