I am using the react autosuggest library to build auto-suggestion
import Autosuggest from "react-autosuggest";
import React, { Component } from "react";
import QueryString from "query-string";
class AutoSuggestSearch extends Component {
constructor() {
super();
this.state = {
value: "",
suggestions: []
};
this.getSuggestionValue = this.getSuggestionValue.bind(this);
this.renderSuggestion = this.renderSuggestion.bind(this);
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
getSuggestionValue = suggestion => suggestion.fullNameSuggestion;
renderSuggestion = suggestion => <div>{suggestion.name}</div>;
onSuggestionSelected = (event, { suggestion}) => {
console.log(suggestion);
this.setState({
suggestions: [],
value: suggestion.name
});
};
onSuggestionsFetchRequested = ({ value }) => {
const params = {
stationPrefixName: value
};
const queryParams = QueryString.stringify(params);
fetch(`http://localhost:8000/api/suggest?${queryParams}`)
.then(res => res.json())
.then(data => {
console.log(data);
this.setState({
suggestions: data
});
})
.catch(console.log);
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: [],
value: ""
});
};
render() {
const { value, suggestions } = this.state;
const inputProps = {
placeholder: "Search",
value,
onChange: this.onChange
};
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
inputProps={inputProps}
/>
);
}
}
export default AutoSuggestSearch;
The suggestion gets rendered on typing on search box as well as the logging inside onSuggestionSelected gets logged correctly but the input search box does not update correctly.
On debugging further I found that onSuggestionsClearRequested also gets invoked after onSuggestionSelected which is causing the search input box to be empty.
I validated this by adding const string inside onSuggestionsClearRequested
onSuggestionsClearRequested = () => {
alert("clear request");
this.setState({
suggestions: [],
value: "mysearch"
});
};
Is there anyway to prevent onSuggestionsClearRequested invokation on suggestion selection?
Or updating the search query value inside onSuggestionsClearRequested is the correct way?
You can use componentDidUpdate or UseEffect if you are using it in functional component.
I have used react-autosuggest in functional component and clear suggestion works only if value doesn't matches with the suggestions:
const [clear, setClear] = useState(false);
const handleOnChange = newValue => {
setValue(newValue);
setClear(false);
};
useEffect(() => {
if (!suggestions.some(option => option === value) && clear) {
setValue('');
}
}, [clear]);
const onSuggestionsClearRequested = () => {
setClear(true);
setSuggestions([]);
};
The onSuggestionsClearRequested function gets called everytime you click outside the search input, which is the default implementation of the libary being used,
What we implement in onSuggestionsClearRequested is upto us.
you can change the implementation as follows :
Approach keep keyword inside input if available options are not selected
onSuggestionsClearRequested = () => {};
this should provide the desired implementation behaviour.
Hi you may approach with hooks. It looks good and less coding.
You may find below
https://github.com/rajmaravanthe/react-auto-suggestion
Related
What the code does: It's performing a DOM search based on what's typed in an input (it's searching elements by text). All this is happening in a React component.
import { useEffect, useReducer } from "react";
let elements: any[] = [];
const App = () => {
const initialState = { keyEvent: {}, value: "Initial state" };
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates }),
initialState
);
function handleInputChange(event: any) {
updateState({ value: event.target.value });
}
function isCommand(event: KeyboardEvent) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event: any) {
if (isCommand(event)) {
updateState({ keyEvent: event });
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
useEffect(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
elements = Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
}
);
console.log('elements 1:', elements)
}, [state]);
console.log('elemets 2:', elements)
return (
<div>
<input
id="input"
type="text"
onChange={handleInputChange}
value={state.value}
/>
<div id="count">{elements.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
export default App;
The problem: The value of elements outside of useEffect is the old data. For example, if you type a in the input, console.log('elements 1:', elements) will log 1, and console.log('elements 2:', elements) will log 0. Note: there are 3 buttons, and one of them has the text a.
The strange thing is that the problem doesn't happen if you comment out this if-statement:
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
In this case, if you type anything (since the pattern matching has been commented out), console.log('elements 1:', elements) and console.log('elements 2:', elements) will log 3. Note: there are 3 buttons.
Question: What could be the problem, and how to fix it? I want to render the current length of elements.
Live code:
It's happening because of the elements variable is not a state, so it's not reactive.
Create a state for the elements:
const [elements, setElements] = useState<HTMLButtonElement[]>([])
And use this state to handle the elements.
import { useEffect, useReducer, useState } from "react";
const App = () => {
const initialState = { keyEvent: {}, value: "Initial state" };
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates }),
initialState
);
const [elements, setElements] = useState<HTMLButtonElement[]>([])
function handleInputChange(event: any) {
updateState({ value: event.target.value });
}
function isCommand(event: KeyboardEvent) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event: any) {
if (isCommand(event)) {
updateState({ keyEvent: event });
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
useEffect(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
let newElements = Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
}
);
setElements(newElements)
console.log("elements 1:", elements?.length);
}, [state]);
console.log("elemets 2:", elements?.length);
return (
<div>
<input
id="input"
type="text"
onChange={handleInputChange}
value={state.value}
/>
<div id="count">{elements?.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
export default App;
Your useEffect() runs after your component has rendendered. So the sequence is:
You type something into input, that triggers handleInputChange
handleInputChange then updates your state using updateState()
The state update causes a rerender, so App is called App()
console.log('elemets 2:', elements.length) runs and logs elements as 0 as it's still empty
App returns the new JSX
Your useEffect() callback runs, updating elements
Notice how we're only updating the elements after you've rerendered and App has been called.
The state of your React app should be used to describe your UI in React. Since elements isn't React state, it has a chance of becoming out of sync with the UI (as you've seen), whereas using state doesn't have this issue as state updates always trigger a UI update. Consider making elements part of your state. If it needs to be accessible throughout your entire App, you can pass it down as props to children components, or use context to make it accessible throughout all your components.
With that being said, I would make the following updates:
Add elements to your state
Remove your useEffect() with the dependency of [state]. If we were to update the elements state within this effect, then that would trigger another rerender directly after the one we just did for the state update. This isn't efficient, and instead, we can tie the update directly to your event handler. See You Might Not Need an Effect for more details and dealing with other types of scenarios:
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/#babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useEffect, useReducer} = React;
const App = () => {
const initialState = {keyEvent: {}, value: "Initial state", elements: []};
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates}),
initialState
);
function searchDOM(value) {
const selectors = "button";
const pattern = new RegExp(value === "" ? "^$" : value);
return Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
return nodeWithText?.textContent?.match(pattern);
}
return false;
}
);
}
function handleInputChange(event) {
updateState({
value: event.target.value,
elements: searchDOM(event.target.value)
});
}
function isCommand(event) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event) {
if (isCommand(event)) {
updateState({
keyEvent: event
});
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
console.log("elements:", state.elements.length);
return (
<div>
<input id="input" type="text" onChange={handleInputChange} value={state.value} />
<div id="count">{state.elements.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
ReactDOM.createRoot(document.body).render(<App />);
</script>
useEffect triggered after react completed its render phase & flush the new changes to the DOM.
In your case you have two useEffects. The first one register your event lister which will then update your component state when input field change. This triggers a state update.( because of the setState )
So React will start render the component again & finish the cycle. And now you have 2nd useEffect which has state in dependency array. Since the state was updated & the new changes are committed to the DOM, react will execute 2nd useEffect logic.
Since your 2nd useEffect just assign some values to a normal variable React will not go re render your component again.
Based on your requirement you don't need a 2nd useEffect. You can use a useMemo,
let elements = useMemo(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
return Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
})
}, [state])
Note: You don't need to assign your elements into another state. It just create another unwanted re render in cycle. Since you are just doing a calculation to find out the element array you can do it with the useMemo
I have multiple setState(newState, callback) statements in my Class component. Now that I'm shifting to using hooks, I am unable to put all of the callbacks in a useEffect and call them conditionally.
Some setState calls are not distinguishable with regard to what changes they are making (they may be changing the same array stored in the useState) but they all fire very different callbacks. I cannot simply just put different conditions in useState and fire callbacks.
The whole logic is becoming much more complex. How have you handled changing setState to useState without affecting the callbacks or having to make very complex logic changes inside useEffect?
Example code:
resetSelections = () => {
const { filterData, toggleSidebarOnReset, toggleSidebar } = this.props;
this.setState(
{
selections: getSelectionsFromFilterData(filterData),
},
() => {
this.triggerAdvancedFilter();
if (toggleSidebarOnReset && toggleSidebar) {
toggleSidebar();
}
if (this.props.removeFilterDefaultSelection) {
this.props.removeFilterDefaultSelection();
}
}
);
};
addCustomField = filterGroupData => {
this.setState(
prevState => ({
customSelectionsMap: {
...prevState.customSelectionsMap,
[filterGroupData.name]: filterGroupData.id,
},
selections: {
...prevState.selections,
[filterGroupData.name]: [],
},
}),
() => this.props.addCustomFieldInFilterData(filterGroupData)
);
};
removeCustomField = data => {
const { selections, customSelectionsMap } = this.state;
const newSelections = { ...selections };
const newCustomSelectionsMap = { ...customSelectionsMap };
delete newSelections[data.name];
delete newCustomSelectionsMap[data.name];
this.setState(
{
selections: newSelections,
customSelectionsMap: newCustomSelectionsMap,
},
() => {
this.props.removeCustomFieldFromFilterData(data);
this.triggerAdvancedFilter();
}
);
};
addToSelection = ({ group, name }, isReplace) => {
const { selections } = this.state;
if (R.contains(name, selections[group])) return null;
const pushState = isReplace ? '$set' : '$push';
this.setState(
prevState => ({
selections: update(prevState.selections, {
[group]: { [pushState]: [name] },
}),
}),
() => this.triggerAdvancedFilter()
);
};
You can apply your callback implementation in useEffect by giving your state variable in dependency array
You can refer to this also
How to use `setState` callback on react hooks
I've tried quite a few methods but I have not been able to get onChange to work. I'm working on a search-bar component that makes a fetch call after the user has not changed the search bar input for 3 seconds, but I am having issues changing the userSearchInput state hook which fires the api call in useEffect. Here is a minimized version of the code:
import React, { useState, useEffect } from "react";
import Select from "react-select";
export default function SearchBar() {
const [userSearchInput, setUserSearchInput] = useState("");
const [searchSuggestions, setSearchSuggestions] = useState([]);
useEffect(() => {
const searchSuggestions = async (searchInput) => {
console.log("api called");
const searchSuggestions = await fetch(
'API STUFF'
)
.then((response) => response.json())
.then((data) => {
setSearchSuggestions(data.quotes);
});
};
const timer = setTimeout(() => {
if (userSearchInput !== "") {
searchSuggestions("test");
}
}, 3000);
return () => clearTimeout(timer);
}, [userSearchInput]);
const handleSearchInputChange = (event) => {
setUserSearchInput(event.target.value);
console.log("input changed");
};
return (
<Select
options={searchSuggestions}
value={userSearchInput}
placeholder="Search a ticker"
onChange={handleSearchInputChange}
/>
);
}
Any ideas on where I'm going wrong?
select has value of object containing "label" and "value"
also, react select already returning value in an argument of function so all you have to do is to use it
const handleSearchInputChange = (selectedOptionObj) => {
setUserSearchInput(selectedOptionObj);
console.log("input changed");
};
i can't move my code to child component so how can i solve this problem. so that i can use my api data to my combobox
async getData() {
const PROXY_URL = 'https://cors-anywhere.herokuapp.com/';
const URL = 'my-api';
const res = await axios({
method: 'post', // i get data from post response
url: PROXY_URL+URL,
data: {
id_user : this.props.user.id_user
}
})
const {data} = await res;
this.setState({ user : data.data})
}
componentDidMount = () => {
this.getData()
}
and i send my state to my combobox in child component
<ComboBox
name="pic"
label="Default PIC"
placeholder=""
refs={register({ required: true })}
error={errors.PIC}
message=""
labelFontWeight="400"
datas={this.state.user}
></ComboBox>
combobox code :
right now I just want to be able to console my index data
let ComboBox = props => {
useEffect(() => {
for (let i = 0; i < props.datas.length; i++) {
console.log(i) //this can use if using hard props or manual data
props.datas[i].selected = false;
props.datas[i].show = true;
}
setDatas(props.datas);
document.addEventListener('click', e => {
try {
if (!refDivComboBox.current.contains(e.target)) {
setIsOpen(false);
}
} catch (error) {}
});
unSelectedComboBox();
}, []);
export default ComboBox;
I think you are missing the props.datas dependency in your ComboBox component.
let ComboBox = props => {
useEffect(() => {
for (let i = 0; i < props.datas.length; i++) {
console.log(i) //this can use if using hard props or manual data
props.datas[i].selected = false;
props.datas[i].show = true;
}
setDatas(props.datas);
document.addEventListener('click', e => {
try {
if (!refDivComboBox.current.contains(e.target)) {
setIsOpen(false);
}
} catch (error) {}
});
unSelectedComboBox();
}, [props.datas]); // THIS IS THE DEPENDENCY ARRAY, try adding props.datas here
export default ComboBox;
Here is a brief explanation of useEffect.
Used as componentDidMount():
useEffect(() => {}, [])
Used as componentDidUpdate() (triggers after props.something changes):
useEffect(() => {}, [props.something])
Used as componenWillUnmount():
useEffect(() => {
return () => { //Unmount }
}, [])
This, of course, is a really simple explanation, and this can be used much better when properly learned. Take a look at some tutorials utilizing useState, try to find in particular migrations from this.state to useState - those might help you wrap your head around useState
I am working on creating a dynamic search results workflow. I can make the results render with no issue, but can't figure out how best to toggle them off when I delete all the input from the search bar. If you start typing, addresses appear that match, but then as you delete all the way they don't all go away.
My thoughts were to use one of the two parameters in my state variables: showMatches or matches.length. I am struggling to see the final piece to this puzzle. Below is my current code:
App.js
import React, { Component } from 'react';
import { Form, Button, ListGroup } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import Match from './Match';
//import Render from './Render';
const my_data = require('./data/test.json')
class App extends Component {
state = {
links: [],
selectedLink:null,
userLocation: {},
searchInput: "",
showMatches: false,
matches: [],
searchLink:[]
}
componentDidMount() {
fetch('https://data.cityofnewyork.us/resource/s4kf-3yrf.json')
.then(res=> res.json())
.then(res=>
//console.log(json)
this.setState({links:res})
);
}
handleInputChange = (event) => {
console.log(event.target.value)
event.preventDefault()
this.setState({searchInput: event.target.value })
this.updateMatches()
console.log(this.state.showMatches)
console.log(this.state.matches.length)
}
handleSubmit = (event) => {
event.preventDefault()
this.displayMatches();
}
findMatches = (wordToMatch, my_obj) => {
return my_obj.filter(place => {
// here we need to figure out the matches
const regex = new RegExp(wordToMatch, 'gi');
//console.log(place.street_address.match(regex))
return place.street_address.match(regex)
});
}
updateMatches =() => {
const matchArray = this.findMatches(this.state.searchInput, this.state.links);
const newStateMatches = matchArray.map(place => {
//console.log(place.street_address);
return place
});
this.setState({matches:newStateMatches})
this.state.matches.length > 1 ? this.setState({showMatches: true}) : this.setState({showMatches: false})
}
alertClicked = address => {
//event.preventDefault(); // not sure what event you're preventing
this.setState({searchLink: address});
this.pushData();
}
render() {
return (
<div>
<input
placeholder="Search for a Link Near you..."
onChange = {this.handleInputChange}
value = {this.state.searchInput}
/>
<ListGroup defaultActiveKey="#link1">
{
this.state.matches.map(match => {
return <Match
address={match.street_address}
alertClicked={this.alertClicked}
value = {this.state.searchLink}
logState={this.logState}/>
})
}
</ListGroup>
</div>
);
}
}
export default App;
Match.js
import React from 'react';
import { ListGroup } from 'react-bootstrap';
const match = ({ alertClicked, address }) => {
return (
<ListGroup.Item
className="Matches"
action
// function expressions could cause this to rerender unnecessarily.
onClick={(address) => alertClicked(address)}>
<p>{`${address}`}</p>
</ListGroup.Item>
)
}
export default match;
Appreciate the help.
The simplest way I think you could implement this is in your handleInputChange, like so :
handleInputChange = (event) => {
event.preventDefault()
if (event.target.value.length === 0) {
this.setState({searchInput: "", showMatches: false, matches: [] })
return
}
this.setState({searchInput: event.target.value })
this.updateMatches()
}
But what do you mean by "but then as you delete all the way they don't all go away" ? Sounds like there actually might be a bug in you updateMatches.
EDIT: Chris' comment is spot on regarding updateMatches.