I'm trying to build an input component with a clear button using react#17
import { useRef } from 'react';
const InputWithClear = props => {
const inputRef = useRef();
return (
<div>
<input
ref={inputRef}
{...props}
/>
<button
onClick={() => {
inputRef.current.value = '';
inputRef.current.dispatchEvent(
new Event('change', { bubbles: true })
);
}}
>
clear
</button>
</div>
);
};
using this component like:
<InputWithClear value={value} onChange={(e) => {
console.log(e); // I want to get a synthetic event object here
}} />
but the clear button works once only when I did input anything first, and stop working again.
if I input something first and then click the clear button, it does not work.
why not using?
<button
onClick={() => {
props.onChange({
target: { value: '' }
})
}}
>
clear
</button>
because the synthetic event object will be lost
So, how do I manually trigger a synthetic change event of a react input component?
Try this approach,
Maintain state at the parent component level (Here parent component is App), onClear, bubble up the handler in the parent level, and update the state.
import React, { useState } from "react";
import "./styles.css";
const InputWithClear = (props) => {
return (
<div>
<input {...props} />
<button onClick={props.onClear}>clear</button>
</div>
);
};
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<InputWithClear
value={value}
onChange={(e) => {
console.log(e); // I want to get a synthetic event object here
setValue(e.target.value);
}}
onClear={() => {
setValue("");
}}
/>
</div>
);
}
Working code - https://codesandbox.io/s/youthful-euler-gx4v5?file=/src/App.js
you should use state to control input value rather than create useRef, that's the way to go. you can use a stopPropagation prop to control it:
const InputWithClear = ({value, setValue, stopPropagation = false}) => {
const onClick = (e) => {
if(stopPropagation) e.stopPropagation()
setValue('')
}
return (
<div>
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
<button
onClick={onClick}
>
clear
</button>
</div>
);
};
export default function App() {
const [value, setValue] = useState('')
return (
<div className="App">
<InputWithClear value={value} setValue={setValue} stopPropagation />
</div>
);
}
Related
In a form that I am making the material that is being created in the form should have multiple width options that can be added. This means that I will have a text input where the user can add an option, and when this option is added, it should be added to the React Hook Form widthOptions array, without using the regular react state. How would one do this? How do you add an item to the total React Hook Form state, I only see options for just one input field corresponding to a property.
This is how i would do it using the regular React state
import { TrashIcon } from "#heroicons/react/24/outline";
import React, { useRef, useState } from "react";
const Test = () => {
const [widthOptions, setWidthOptions] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const removeWidthOption = (widthOption: string) => {
setWidthOptions(widthOptions.filter((option) => option !== widthOption));
};
const addWidthOption = (widthOption: string) => {
setWidthOptions([...widthOptions, widthOption]);
};
const editWidthOptions = (widthOption: string, index: number) => {
const newWidthOptions = [...widthOptions];
newWidthOptions[index] = widthOption;
setWidthOptions(newWidthOptions);
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={() => addWidthOption(inputRef?.current?.value)}>
Add Width Option
</button>
{widthOptions.map((option, index) => (
<div className="flex">
<input
type="text"
value={option}
onChange={() => editWidthOptions(option, index)}
/>
<button type="button" onClick={() => removeWidthOption(option)}>
<TrashIcon className="w-5 h-5 mb-3 text-gray-500" />
</button>
</div>
))}
</div>
);
};
export default Test;
You can just the controller component for this as for all other fields.
Since you have not shared any of you code here is a generic multi-select
<Controller
name={name}
render={({ field: { value, onChange, ref } }) => {
return (
// You can use whatever component you want here, the you get the value from the form and use onChange to update the value as you would with a regular state
<Test
widthOptions={value}
setWidthOptions={onChange}
/>
);
}}
/>;
https://react-hook-form.com/api/usecontroller/controller/
And in you Test component remove the state and get the props instead
const Test = ({widthOptions, setWidthOptions}) => {
const inputRef = useRef<HTMLInputElement>(null);
.
.
.
I am using react instant search library and my issue is that my custom refinement list components loses its selections when I open modal.
I control my modal with useState:
const [modalIsOpen, setModalIsOpen] = useState(false);
Everytime I call setModalIsOpen(true); the refinements reset.
My custom refinement list component:
const RefinementList = ({ items, refine }: RefinementListProvided) => {
// return the DOM output
return (
<div className="">
{items.map(({ value, label, count, isRefined }: any) => (
<div key={value}>
<motion.button
onClick={() => {
refine(value);
}}
className={``}
>
<div className="">
{label}
</div>
</motion.button>
</div>
))}
</div>
);
};
I connect it with connectRefinementList
const CustomRefinementList = connectRefinementList(RefinementList);
This is my main jsx:
<InstantSearch searchClient={searchClient} indexName="foods">
<CustomSearchBox />
<CustomRefinementList
transformItems={(items) => orderBy(items, "label", "asc")} // this prevents facets jumping
attribute="tags"
/>
<InfiniteHits hitComponent={Hit} cache={sessionStorageCache} />
<ModalForMealPreview
handleOpen={modalIsOpen}
handleClose={handleModalClose}
/>
</InstantSearch>
What can I do to persist state or prevent RefinementList component from rerendering?
Here is a basic Example of React.memo, this will help your code
import React, { useEffect, useState } from "react";
const MemoComp = React.memo(({ ...props }) => <Test {...props} />); // Main Area to watch
function ClassSearch() {
const [state, setState] = useState(1);
return (
<div>
<button onClick={() => setState(state + 1)}>Increase</button> <br />
<MemoComp data="memorized" /> <br />
<Test data="original" /> <br />
</div>
);
}
export default ClassSearch;
const Test = ({ data }) => {
const date = new Date().getTime();
return (
<>
Test {date} {data}
</>
);
};
How to keep input focus when changing state inside onFocus event
seems like it's not happening with simple input
only when using customInput
const CustomInput = ({ onFocus, state }) => {
return (
<div>
<input onFocus={onFocus} />
</div>
);
};
const TestInput = () => {
const [state, setState] = React.useState(false);
return (
<FilledInput
inputComponent={(props) => {
return (
<CustomInput onFocus={props.onFocus} state={state} />
);
}}
onFocus={() => {
setState(true);
}}
/>
);
};
I have designed a component MyCheckBox ( i used it as helper component ), so I imported it in another component, but whenever i am trying to trigger the event, it not triggered.
Code for reference
MyCheck Box
interface MyCheckBoxProps
{
onClick?:MouseEventHandler<HTMLInputElement>;
checked?:boolean
}
export const MyCheckBox (props:MyCheckBoxProps): JSX.Element =>{
return(
<input
type="checkbox"
id={id}
onClick={onClick}
checked={checked}
/>
)
}
When I Imported it
const[check,setCheck] = useState(false)
const handleCheckChange = (e: any, key: string) => {
console.log("check", e) // => this console log does not show up
if (key === "newPostActivity") {
console.log("check", e)
setCheck(e.target.checked)
}
}
<MyCheckBox checked={check} onClick={(e)=>handleCheckChange(e,"some string")}/>
Whenever I click on MyCheckBox , the event ( handleCheckChange )is never triggered
Anybody please help?
you are calling
<MyComponent checked={check}
onClick={(e) => handleCheckChange(e, "some string")}/>
in your App component which is the parent component. Here your checked and onclick are the props. so now accessing the props in your <MyComponent you have to make sure to call it as
import React from "react";
export default function MyCheckBox(props) {
return (
<input type="checkbox" onClick={props.onClick} checked={props.checked} />
);
}
OR
destructrize your props like
import React from "react";
export default function MyCheckBox({onClick, checked}) {
return (
<input type="checkbox" onClick={onClick} checked={checked} />
);
}
I don't understand if you wrote this code as an example of a not working one. If this is the final code, you must destruct the props to use like that.
<input
type="checkbox"
id={id}
onClick={onClick}
checked={checked}
/>
Otherwise, you should call:
<input
type="checkbox"
id={props.id}
onClick={props.onClick}
checked={props.checked}
/>
I've rewrote your code:
App
export default function App() {
const [check, setCheck] = useState(false);
const handleCheckChange = (e: MouseEvent<HTMLInputElement>, key: string) => {
console.log("check", e);
setCheck(!check);
console.log("I am state", check);
};
return (
<div>
<MyCheckBox
id="foo"
checked={check}
onClick={(e) => handleCheckChange(e, "some string")}
/>
</div>
);
}
MyCheckBox
interface MyCheckBoxProps {
id: string;
onClick?: MouseEventHandler<HTMLInputElement>;
checked?: boolean;
}
const MyCheckBox: React.FC<MyCheckBoxProps> = (props) => {
const { id, onClick, checked } = props;
return <input type="checkbox" id={id} onClick={onClick} checked={checked} />;
};
export default MyCheckBox;
Copied from my codesandbox
I have a form page structured more or less as follows:
<Layout>
<Page>
<Content>
<Input />
<Map />
</Content>
</Page>
<Button />
</Layout>
The Map component should only be rendered once, as there is an animation that is triggered on render. That means that Content, Page and Layout should not re-render at all.
The Button inside Layout should be disabled when the Input is empty. The value of the Input is not controlled by Content, as a state change would cause a re-render of the Map.
I've tried a few different things (using refs, useImperativeHandle, etc) but none of the solutions feel very clean to me. What's the best way to go about connecting the state of the Input to the state of the Button, without changing the state of Layout, Page or Content? Keep in mind that this is a fairly small project and the codebase uses "modern" React practices (e.g. hooks), and doesn't have global state management like Redux, MobX, etc.
Here is an example (click here to play with it) that avoids re-render of Map. However, it re-renders other components because I pass children around. But if map is the heaviest, that should do the trick. To avoid rendering of other components you need to get rid of children prop but that most probably means you will need redux. You can also try to use context but I never worked with it so idk how it would affect rendering in general
import React, { useState, useRef, memo } from "react";
import "./styles.css";
const GenericComponent = memo(
({ name = "GenericComponent", className, children }) => {
const counter = useRef(0);
counter.current += 1;
return (
<div className={"GenericComponent " + className}>
<div className="Counter">
{name} rendered {counter.current} times
</div>
{children}
</div>
);
}
);
const Layout = memo(({ children }) => {
return (
<GenericComponent name="Layout" className="Layout">
{children}
</GenericComponent>
);
});
const Page = memo(({ children }) => {
return (
<GenericComponent name="Page" className="Page">
{children}
</GenericComponent>
);
});
const Content = memo(({ children }) => {
return (
<GenericComponent name="Content" className="Content">
{children}
</GenericComponent>
);
});
const Map = memo(({ children }) => {
return (
<GenericComponent name="Map" className="Map">
{children}
</GenericComponent>
);
});
const Input = ({ value, setValue }) => {
const onChange = ({ target: { value } }) => {
setValue(value);
};
return (
<input
type="text"
value={typeof value === "string" ? value : ""}
onChange={onChange}
/>
);
};
const Button = ({ disabled = false }) => {
return (
<button type="button" disabled={disabled}>
Button
</button>
);
};
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>SO Q#60060672</h1>
<Layout>
<Page>
<Content>
<Input value={value} setValue={setValue} />
<Map />
</Content>
</Page>
<Button disabled={value === ""} />
</Layout>
</div>
);
}
Update
Below is version with context that does not re-render components except input and button:
import React, { useState, useRef, memo, useContext } from "react";
import "./styles.css";
const ValueContext = React.createContext({
value: "",
setValue: () => {}
});
const Layout = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Layout rendered {counter.current} times</div>
<Page />
<Button />
</div>
);
});
const Page = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Page rendered {counter.current} times</div>
<Content />
</div>
);
});
const Content = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Content rendered {counter.current} times</div>
<Input />
<Map />
</div>
);
});
const Map = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Map rendered {counter.current} times</div>
</div>
);
});
const Input = () => {
const { value, setValue } = useContext(ValueContext);
const onChange = ({ target: { value } }) => {
setValue(value);
};
return (
<input
type="text"
value={typeof value === "string" ? value : ""}
onChange={onChange}
/>
);
};
const Button = () => {
const { value } = useContext(ValueContext);
return (
<button type="button" disabled={value === ""}>
Button
</button>
);
};
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>SO Q#60060672, method 2</h1>
<p>
Type something into input below to see how rendering counters{" "}
<s>update</s> stay the same
</p>
<ValueContext.Provider value={{ value, setValue }}>
<Layout />
</ValueContext.Provider>
</div>
);
}
Solutions rely on using memo to avoid rendering when parent re-renders and minimizing amount of properties passed to components. Ref's are used only for render counters
I have a sure way to solve it, but a little more complicated.
Use createContext and useContext to transfer data from layout to input. This way you can use a global state without using Redux. (redux also uses context by the way to distribute its data). Using context you can prevent property change in all the component between Layout and Imput.
I have a second easier option, but I'm not sure it works in this case. You can wrap Map to React.memo to prevent render if its property is not changed. It's quick to try and it may work.
UPDATE
I tried out React.memo on Map component. I modified Gennady's example. And it works just fine without context. You just pass the value and setValue to all component down the chain. You can pass all property easy like: <Content {...props} /> This is the easiest solution.
import React, { useState, useRef, memo } from "react";
import "./styles.css";
const Layout = props => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Layout rendered {counter.current} times</div>
<Page {...props} />
<Button {...props} />
</div>
);
};
const Page = props => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Page rendered {counter.current} times</div>
<Content {...props} />
</div>
);
};
const Content = props => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Content rendered {counter.current} times</div>
<Input {...props} />
<Map />
</div>
);
};
const Map = memo(() => {
const counter = useRef(0);
counter.current += 1;
return (
<div className="GenericComponent">
<div className="Counter">Map rendered {counter.current} times</div>
</div>
);
});
const Input = ({ value, setValue }) => {
const counter = useRef(0);
counter.current += 1;
const onChange = ({ target: { value } }) => {
setValue(value);
};
return (
<>
Input rendedred {counter.current} times{" "}
<input
type="text"
value={typeof value === "string" ? value : ""}
onChange={onChange}
/>
</>
);
};
const Button = ({ value }) => {
const counter = useRef(0);
counter.current += 1;
return (
<button type="button" disabled={value === ""}>
Button (rendered {counter.current} times)
</button>
);
};
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>SO Q#60060672, method 2</h1>
<p>
Type something into input below to see how rendering counters{" "}
<s>update</s> stay the same, except for input and button
</p>
<Layout value={value} setValue={setValue} />
</div>
);
}
https://codesandbox.io/s/weathered-wind-wif8b