Using React useRef Hook - javascript

I have been building Select Component of my own using button components in React. I am stuck on how to close the menu when the user clicks outside the menu. The code sandbox can be found here:
I am thinking of adding an event listener when the component is mounted, which would track whether the click was made inside of the menu or not. I am expecting to solve this using useRef Hook.

Try this:
import { useEffect, useRef, useState } from "react";
import "./styles.css";
export default function App() {
const options = [
{
label: "Apple",
value: "apple"
},
{
label: "Ball",
value: "ball"
},
{
label: "Car",
value: "car"
}
];
return (
<div className="App">
<SelectComponent options={options} />
</div>
);
}
function SelectComponent(props) {
const ref = useRef(null)
const [selectedVal, setSelectedVal] = useState("");
const [isMenuOpen, setIsMenuOpen] = useState(false);
useEffect(() => {
document.addEventListener("click", (e) => {
//Insert code here.
});
}, []);
useEffect(() => {
document.addEventListener('click', handleClickOutside, false)
return () => document.removeEventListener('click', handleClickOutside, false)
})
const handleClickOutside = (e) => {
if (ref.current && !ref.current.contains(e.target)) setIsMenuOpen(false)
}
const onSelectClick = () => {
setIsMenuOpen((prevState) => !prevState);
};
const onValueSelect = (e) => {
const {
target: { id }
} = e;
setSelectedVal(id);
setIsMenuOpen((prevState) => !prevState);
};
const computeClass = isMenuOpen ? "close-icon" : "open-icon";
return (
<div {...{ref}}>
<button
className={`select-btn selected-val ${computeClass}`}
onClick={onSelectClick}
>
{selectedVal}
</button>
{isMenuOpen &&
props.options.map((item) => {
return (
<button
key={item.value}
id={item.label}
value={item.value}
className={"select-btn"}
onClick={onValueSelect}
>
{item.label}
</button>
);
})}
</div>
);
}

Simple Way to handle the useRef in the component is shown below :-
Here our target is to resize the input filed on submit button click.
import React, { useState, useRef } from "react";
const UseRef = () => {
const inputRef = useRef();
const [name, setName] = useState("");
const handleSize = ()=>{
console.log("submit");
inputRef.current.style.width="300px"
}
return (
<div>
<label>Name:</label>
<input
ref={inputRef}
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
style={{ padding: "10px" }}
/>
<br />
<button onClick={handleSize} >submit</button>
</div>
);
};
export default UseRef;
inputRef is the instance of the useRef which I have passed in the specific DOM element which we are going to manipulate on click event of submit button.
inputRef.current.style.width="300px" this is the manipulation done with the DOM element which is passed in the function handleSize()
When onClcik event is invoked from submit button size of the button will be increased to 300px .
We can also get the same result by avoiding import of useState & by eleminating onChange & value props from the input filed.

Related

How can I send the state (useState) of one file component to another file's component?

REACT.js:
Let say I have a home page with a search bar, and the search bar is a separate component file i'm calling.
The search bar file contains the useState, set to whatever the user selects. How do I pull that state from the search bar and give it to the original home page that
SearchBar is called in?
The SearchBar Code might look something like this..
import React, { useEffect, useState } from 'react'
import {DropdownButton, Dropdown} from 'react-bootstrap';
import axios from 'axios';
const StateSearch = () =>{
const [states, setStates] = useState([])
const [ stateChoice, setStateChoice] = useState("")
useEffect (()=>{
getStates();
},[])
const getStates = async () => {
let response = await axios.get('/states')
setStates(response.data)
}
const populateDropdown = () => {
return states.map((s)=>{
return (
<Dropdown.Item as="button" value={s.name}>{s.name}</Dropdown.Item>
)
})
}
const handleSubmit = (value) => {
setStateChoice(value);
}
return (
<div>
<DropdownButton
onClick={(e) => handleSubmit(e.target.value)}
id="state-dropdown-menu"
title="States"
>
{populateDropdown()}
</DropdownButton>
</div>
)
}
export default StateSearch;
and the home page looks like this
import React, { useContext, useState } from 'react'
import RenderJson from '../components/RenderJson';
import StateSearch from '../components/StateSearch';
import { AuthContext } from '../providers/AuthProvider';
const Home = () => {
const [stateChoice, setStateChoice] = useState('')
const auth = useContext(AuthContext)
console.log(stateChoice)
return(
<div>
<h1>Welcome!</h1>
<h2> Hey there! Glad to see you. Please login to save a route to your prefered locations, or use the finder below to search for your State</h2>
<StateSearch stateChoice={stateChoice} />
</div>
)
};
export default Home;
As you can see, these are two separate files, how do i send the selection the user makes on the search bar as props to the original home page? (or send the state, either one)
You just need to pass one callback into your child.
Homepage
<StateSearch stateChoice={stateChoice} sendSearchResult={value => {
// Your Selected value
}} />
Search bar
const StateSearch = ({ sendSearchResult }) => {
..... // Remaining Code
const handleSubmit = (value) => {
setStateChoice(value);
sendSearchResult(value);
}
You can lift the state up with function you pass via props.
const Home = () => {
const getChoice = (choice) => {
console.log(choice);
}
return <StateSearch stateChoice={stateChoice} giveChoice={getChoice} />
}
const StateSearch = (props) => {
const handleSubmit = (value) => {
props.giveChoice(value);
}
// Remaining code ...
}
Actually there is no need to have stateChoice state in StateSearch component if you are just sending the value up.
Hello and welcome to StackOverflow. I'd recommend using the below structure for an autocomplete search bar. There should be a stateless autocomplete UI component. It should be wrapped into a container that handles the search logic. And finally, pass the value to its parent when the user selects one.
// import { useState, useEffect } from 'react' --> with babel import
const { useState, useEffect } = React // --> with inline script tag
// Autocomplete.jsx
const Autocomplete = ({ onSearch, searchValue, onSelect, suggestionList }) => {
return (
<div>
<input
placeholder="Search!"
value={searchValue}
onChange={({target: { value }}) => onSearch(value)}
/>
<select
value="DEFAULT"
disabled={!suggestionList.length}
onChange={({target: {value}}) => onSelect(value)}
>
<option value="DEFAULT" disabled>Select!</option>
{suggestionList.map(({ id, value }) => (
<option key={id} value={value}>{value}</option>
))}
</select>
</div>
)
}
// SearchBarContainer.jsx
const SearchBarContainer = ({ onSelect }) => {
const [searchValue, setSearchValue] = useState('')
const [suggestionList, setSuggestionList] = useState([])
useEffect(() => {
if (searchValue) {
// some async logic that fetches suggestions based on the search value
setSuggestionList([
{ id: 1, value: `${searchValue} foo` },
{ id: 2, value: `${searchValue} bar` },
])
}
}, [searchValue, setSuggestionList])
return (
<Autocomplete
onSearch={setSearchValue}
searchValue={searchValue}
onSelect={onSelect}
suggestionList={suggestionList}
/>
)
}
// Home.jsx
const Home = ({ children }) => {
const [result, setResult] = useState('')
return (
<div>
<SearchBarContainer onSelect={setResult} />
result: {result}
</div>
)
}
ReactDOM.render(<Home />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Just pass a setState to component
parent component:
const [state, setState] = useState({
selectedItem: ''
})
<StateSearch state={state} setState={setState} />
change parent state from child component:
const StateSearch = ({ state, setState }) => {
const handleStateChange = (args) => setState({…state, selectedItem:args})
return (...
<button onClick={() => handleStateChange("myItem")}/>
...)
}

Update a component after useState value updates

Having a monaco-editor inside a React component:
<Editor defaultValue={defaultValue} defaultLanguage='python' onChange={onChangeCode} />
The defaultValue, the default code inside of the editor, is sent via props to the component:
const MyComponent = ({
originalCode
}: MyComponentProps) => {
const [defaultValue, setDefaultValue] = useState(originalCode);
When the user edits the code, onChange={onChangeCode} is called:
const onChangeCode = (input: string | undefined) => {
if (input) {
setCode(input);
}
};
My question is, how to reset the code to the original one when the user clicks on Cancel?
Initially it was like:
const handleCancel = () => {
onChangeCode(defaultValue);
};
but it didn't work, probably because useState is asynchronous, any ideas how to fix this?
Here is the whole component for more context:
import Editor from '#monaco-editor/react';
import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Button, HeaderWithButtons } from '../shared/ui-components';
import { ICalculationEngine } from '../../../lib/constants/types';
import { usePostScript } from '../../../lib/hooks/use-post-script';
import { scriptPayload } from '../../../mocks/scriptPayload';
import { editorDefaultValue } from '../../../utils/utils';
export interface ScriptDefinitionProps {
realInputDetails: Array<ICalculationEngine['RealInputDetails']>;
realOutputDetails: ICalculationEngine['RealInputDetails'];
originalCode: string;
scriptLibId: string;
data: ICalculationEngine['ScriptPayload'];
}
const ScriptDefinition = ({
realInputDetails,
realOutputDetails,
originalCode
}: ScriptDefinitionProps) => {
const [defaultValue, setDefaultValue] = useState(originalCode);
const [code, setCode] = useState(defaultValue);
const { handleSubmit } = useForm({});
const { mutate: postScript } = usePostScript();
const handleSubmitClick = handleSubmit(() => {
postScript(scriptPayload);
});
const handleCancel = () => {
onChangeCode(defaultValue);
};
const onChangeCode = (input: string | undefined) => {
if (input) {
setCode(input);
}
};
useEffect(() => {
setDefaultValue(editorDefaultValue(realInputDetails, realOutputDetails));
}, [realInputDetails, realOutputDetails, originalCode]);
return (
<div>
<HeaderWithButtons>
<div>
<Button title='cancel' onClick={handleCancel} />
<Button title='save' onClick={handleSubmitClick} />
</div>
</HeaderWithButtons>
<Editor defaultValue={defaultValue} defaultLanguage='python' onChange={onChangeCode} />
</div>
);
};
export default ScriptDefinition;
If you need the ability to change the value externally, you'll need to use the Editor as a controlled component by passing the value prop (sandbox):
For example:
const defaultValue = "// let's write some broken code 😈";
function App() {
const [value, setValue] = useState(defaultValue);
const handleCancel = () => {
setValue(defaultValue);
};
return (
<>
<button title="cancel" onClick={handleCancel}>
Cancel
</button>
<Editor
value={value}
onChange={setValue}
height="90vh"
defaultLanguage="javascript"
/>
</>
);
}

React memo creates rerender

I'm having an issue with react memo when using nextjs. In the _app e.g. I have a button imported:
import { ChildComponent } from './ChildComponent';
export const Button = ({ classN }: { classN?: string }) => {
const [counter, setCounter] = useState(1);
const Parent = () => {
<button onClick={() => setCounter(counter + 1)}>Click me</button>
}
return (
<div>
{counter}
<Parent />
<ChildComponent />
</div>
);
};
Child component:
import React from 'react';
export const ChildComponent = React.memo(
() => {
React.useEffect(() => {
console.log('rerender child component');
}, []);
return <p>Prevent rerender</p>;
},
() => false
);
I made one working in React couldn't figure it out in my own app:
https://codesandbox.io/s/objective-goldwasser-83vb4?file=/src/ChildComponent.js
The second argument of React.memo() must be a function that returns true if the component don't need to be rerendered and false otherwise - or in the original definition, if the old props and the new props are equal or not.
So, in your code, the solution should be just change the second argument to:
export const ChildComponent = React.memo(
() => { ... },
// this
() => true
);
Which is gonna tell React that "the props didn't change and thus don't need to rerender this component".
So my issue was that I made a function called Button and returned inside a button or Link. So I had a mouseEnter inside the button which would update the state and handle the function outside the function. Kinda embarrassing. This fixed it. So the only change was I moved usestate and handlemousehover inside the button function.
const Button = () => {
const [hover, setHover] = useState(false);
const handleMouseHover = (e: React.MouseEvent<HTMLElement>) => {
if (e.type === 'mouseenter') {
setHover(true);
} else if (e.type === 'mouseleave') setHover(false);
};
return (
<StyledPrimaryButton
onMouseEnter={(e) => handleMouseHover(e)}
onMouseLeave={(e) => handleMouseHover(e)}
>
<StyledTypography
tag="span"
size="typo-20"
>
{title}
</StyledTypography>
<ChildComponent />
</StyledPrimaryButton>
);
};

Why does state only update after invoking twice using React Hooks?

I'm stuck trying to understand why my state won't update until I change the value in the text input twice (calling the handleChange function). What am I doing wrong here?
import React, {useEffect, useState} from "react";
export default function Typeahead(props){
const {list} = props;
const [colorList] = useState(list.map(element => element.toLowerCase()));
const [color,setColor] = useState();
const [showResults, setShowResults]= useState(false);
const [results,setResults]= useState();
let handleChange = (e) =>{
setShowResults(true);
setColor(e.target.value.toLowerCase());
const match = (colorList) =>{
return colorList.startsWith(color,0);
};
const matches = colorList.filter(match);
setResults((matches));
console.log(results);
console.log(showResults);
};
useEffect(() => {
//setResults(list.map(elements => elements.toLowerCase()));
}, [results]);
return(
<div>
<input type= "text" onChange={handleChange}/>
{showResults ?
<div>
{results.map((options) => {
return (
<option key={options} value={options}> {options}</option>
)
})}
</div>
: null }
</div>
);
}

How to debounce a controlled input?

I'm currently struggling with react inputs and debounce from lodash.
Most of the time when I have a form I also have an edit option, so I need a controlled component to fill back the inputs using value={state["targetValue"]} so I can fill and edit the field.
However, if the component is controlled debounce isn't working.
I made a simple example on CodeSandbox: https://codesandbox.io/embed/icy-cloud-ydzj2?fontsize=14&hidenavigation=1&theme=dark
Code:
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { debounce } from "lodash";
import "./styles.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
name: "",
title: "",
editMode: false
};
this.debouncedEvent = React.createRef();
}
debounceEvent(_fn, timer = 500, options = null) {
this.debouncedEvent.current = debounce(_fn, timer, options);
return e => {
e.persist();
return this.debouncedEvent.current(e);
};
}
componentWillUnmount() {
this.debouncedEvent.current.cancel();
}
onChangeValue = event => {
const { name, value } = event.target;
this.setState(() => {
return { [name]: value };
});
};
onRequestEdit = () => {
this.setState({ name: "Abla blabla bla", editMode: true });
};
onCancelEdit = () => {
if (this.state.editMode) this.setState({ name: "", editMode: false });
};
onSubmit = event => {
event.preventDefault();
console.log("Submiting", this.state.name);
};
render() {
const { name, editMode } = this.state;
const isSubmitOrEditLabel = editMode ? `Edit` : "Submit";
console.log("rendering", name);
return (
<div className="App">
<h1> How to debounce controlled input ?</h1>
<button type="button" onClick={this.onRequestEdit}>
Fill with dummy data
</button>
<button type="button" onClick={this.onCancelEdit}>
Cancel Edit Mode
</button>
<div style={{ marginTop: "25px" }}>
<label>
Controlled / Can be used for editing but not with debounce
</label>
<form onSubmit={this.onSubmit}>
<input
required
type="text"
name="name"
value={name}
placeholder="type something"
// onChange={this.onChangeValue}
onChange={this.debounceEvent(this.onChangeValue)}
/>
<button type="submit">{isSubmitOrEditLabel}</button>
</form>
</div>
<div style={{ marginTop: "25px" }}>
<label> Uncontrolled / Can't be used for editing </label>
<form onSubmit={this.onSubmit}>
<input
required
type="text"
name="name"
placeholder="type something"
onChange={this.debounceEvent(this.onChangeValue)}
/>
<button type="submit">{isSubmitOrEditLabel}</button>
</form>
</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
At first it looks impossible, but there is a simple way to do it.
Just create the debounce function expression outside the react component.
Here is a pseudo example, for modern React with Hooks:
import React, { useState, useEffect } from "react";
import { debounce } from "lodash";
...
const getSearchResults = debounce((value, dispatch) => {
dispatch(getDataFromAPI(value));
}, 800);
const SearchData = () => {
const [inputValue, setInputValue] = useState("");
...
useEffect(() => {
getSearchResults(inputValue, dispatch);
}, [inputValue]);
...
return (
<>
<input
type="text"
placeholder="Search..."
value={inputValue}
onChange={e => setInputValue(e.target.value)}
/>
...
</>
);
};
export default SearchData;
Update: In time I came up with a better solution using "useCallback"
import React, { useState, useEffect, useCallback } from "react";
import { debounce } from "lodash";
...
const SearchData = () => {
const [inputValue, setInputValue] = useState("");
...
const getSearchResults = useCallback(
debounce(value => {
dispatch(getDataFromAPI(value));
}, 800),
[]
);
useEffect(() => {
getSearchResults(inputValue);
}, [inputValue]);
...
return (
<>
<input
type="text"
placeholder="Search..."
value={inputValue}
onChange={e => setInputValue(e.target.value)}
/>
...
</>
);
};
So... Apparently, there's no solution. the input takes the value from the state. While debounce prevents the state to trigger.
I made a workaround using ReactDOM.
import ReactDOM from "react-dom";
export const setFormDefaultValue = (obj, ref) => {
if (ref && !ref.current) return;
if (!obj || !obj instanceof Object) return;
const _this = [
...ReactDOM.findDOMNode(ref.current).getElementsByClassName("form-control")
];
if (_this.length > 0) {
_this.forEach(el => {
if (el.name in obj) el.value = obj[el.name];
else console.error(`Object value for ${el.name} is missing...`);
});
}
};
and then the use:
this.refForm = React.createRef();
setFormDefaultValue(this.state, refForm)
This way I can fill my form with the state default value and continue using debounce.
Take a look at this lib: https://www.npmjs.com/package/use-debounce
Here's an example of how to use it:
import React, { useState } from 'react';
import { useDebounce } from 'use-debounce';
export default function Input() {
const [text, setText] = useState('Hello');
const [value] = useDebounce(text, 1000);
return (
<div>
<input
defaultValue={'Hello'}
onChange={(e) => {
setText(e.target.value);
}}
/>
<p>Actual value: {text}</p>
<p>Debounce value: {value}</p>
</div>
);
}
You can try this.
import React, { useState, useCallback, useRef, useEffect } from 'react';
import _ from 'lodash';
function usePrevious(value: any) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const DeboucnedInput = React.memo(({ value, onChange }) => {
const [localValue, setLocalValue] = useState('');
const prevValue = usePrevious(value);
const ref = useRef();
ref.current = _.debounce(onChange, 500);
useEffect(() => {
if (!_.isNil(value) && prevValue !== value && localValue !== value) {
setLocalValue(value);
}
}, [value]);
const debounceChange = useCallback(
_.debounce(nextValue => {
onChange(nextValue);
}, 1000),
[]
);
const handleSearch = useCallback(
nextValue => {
if (nextValue !== localValue) {
setLocalValue(nextValue);
debounceChange(nextValue);
}
},
[localValue, debounceChange]
);
return (
<input
type="text"
value={localValue}
onChange={handleSearch}
/>
);
});
I had a similar issue, useMemo and debounce from lodash seem to work for me.
import React, { useState, useEffect, useMemo } from 'react';
import { debounce } from 'lodash';
const Search = () => {
const [inputValue, setInputValue] = useState('');
const handleChange = (event) => {
setInputValue(event.target.value);
};
const handleFetch = (input) => {
// do stg
};
const debouncedFetch = useMemo(() => debounce(handleFetch, 500), []);
useEffect(() => {
debouncedFetch(inputValue);
}, [inputValue]);
return (
<Form>
<input
type="text"
value={inputValue}
onChange={handleChange}
/>
// rest of the component
</Form>
);
};
Some articles that helped me:
https://dev.to/alexdrocks/using-lodash-debounce-with-react-hooks-for-an-async-data-fetching-input-2p4g
https://www.carlrippon.com/using-lodash-debounce-with-react-and-ts/
https://blog.logrocket.com/how-and-when-to-debounce-or-throttle-in-react/

Categories

Resources