I would like to use Ant Design AutoComplete component to fetch data from API. The component has the following code:
const Example = ({ token, service }) => {
const [value, setValue] = useState('')
const [options, setOptions] = useState([])
const { data } = useFetch(value)
const onSelect = (data) => {
console.log('onSelect', data)
}
const onChange = (query) => {
console.log("Search query ", query);
setValue(query);
console.log("State value ", value);
if (value && value.length > 1) {
setOptions(
data ? data : []
)
} else {
setOptions([])
}
}
return (
<AutoComplete
value={value}
options={options}
onSelect={onSelect}
onChange={onChange}
/>
)
}
data for the options is provided by useFetch(value) hook. So the value should be updated as the user types in the input.
But the problem is that value in the state is always one character behind the actual search query. Here is the link to my codesandbox. You can see that console.log() for search query and state value are always different. Is there any way to fix that? I need state value to be always in synch with the search query.
In this case you can make use of useEffect which can be used to calls API and update options.
You can do following:
React.useEffect(() => {
console.log("Value"); // This will be in sync with your search.
}, [value])
Working sandbox link: https://codesandbox.io/s/basic-usage-autocomplete-forked-eihbw?file=/index.js:369-448
Related
I have used the multiselect-react-dropdown library for implement MultiSelect dropdown. I this library have two props: one is options and second is onSearch. In options props passed the data which is showing on dropdown. user can select the data from it. But onSearch is used on search user's by api. So how can i manage both the data on dropdown. Please help me to solve this.
<Multiselect
onSearch={(e) => callSearchApi(e)}
placeholder="Select Contributor"
onSelect={onSelect}
closeIcon="circle"
options={UsersData}
displayValue="name"
/>
API Call
const callSearchApi = (name) => {
console.log('callSearchApi', name);
collaboratorsSearch(name)
.then((response) => {
console.log('callSearchApi fetch user', response.data.results);
})
.catch((error) => {
console.log('callSearchApi fetch user Error', error);
});
};
To implement this feature you need to use react hook useState here.
You need to set below state and hooks. I am setting options inside useEffect here but in your case you can set it in button click event.
const [ selectedOptionValue, setSelectedOptionValue ] = useState('');
const [ options, setOptions ] = useState([]);
useEffect(() => {
setOptions(Data);
}, [])
You can toggle options data based on your requirement. Set this options variable either from callSearchApi or from props.
I have implemented onSearch and callSearchApi function here. Just to reduce the code I am using async and await.
const callSearchApi = async (value) => {
const response = await fetch('https://jsonplaceholder.typicode.com/users/?name='+ value);
const users = await response.json();
if (users && users.length > 0) {
setOptions(users);
} else {
setOptions(Data);
}
};
const onSearch = (value) => {
console.log('Value', value)
callSearchApi(value);
}
Please note I have used dummy API here which gives users information.
Here is the complete code.
import React, { useState, useEffect } from "react";
import "./style.css";
import Multiselect from 'multiselect-react-dropdown';
import { Data } from "./data";
export default function App() {
const [ selectedOptionValue, setSelectedOptionValue ] = useState('');
const [ options, setOptions ] = useState([]);
useEffect(async () => {
setOptions(Data);
}, [])
const onSelect = (selectedList, selectedItem) => {
}
const onRemove = (selectedList, removedItem) => {
}
const callSearchApi = async (value) => {
const response = await fetch('https://jsonplaceholder.typicode.com/users/?name='+ value);
const users = await response.json();
if (users && users.length > 0) {
setOptions(users);
} else {
setOptions(Data);
}
};
const onSearch = (value) => {
console.log('Value', value)
callSearchApi(value);
}
return (
<div>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
{
// options? options.length : null
options? ( <Multiselect
onSearch={onSearch}
options={options} // Options to display in the dropdown
selectedValues={selectedOptionValue} // Preselected value to persist in dropdown
onSelect={onSelect} // Function will trigger on select event
onRemove={onRemove} // Function will trigger on remove event
displayValue="name" // Property name to display in the dropdown options
/>): null
}
</div>
);
}
You can find working example here on stackblitz.
I have the following react code that pulls some data from an api and outputs it. In result['track_list'] I receive a list of tracks with a timestamp and in aggTrackList() I am aggregating the data into key value pair based on the day/month/year then displaying that aggregated data in a Card component I created.
import React, { useState, useEffect } from "react";
function App() {
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [trackList, settracks] = useState([]);
const [sortby, setSortby] = useState("day");
const [sortedList, setSortedList] = useState([]);
useEffect(() => {
aggTrackList();
}, [sortby]);
const aggTrackList = () => {
setSortedList([]);
let sortedObj = {};
switch (sortby) {
case "day":
trackList.forEach((track) => {
let dayVal = new Date(track[3]).toDateString();
dayVal in sortedObj
? sortedObj[dayVal].push(track)
: (sortedObj[dayVal] = [track]);
});
setSortedList(sortedObj);
break;
case "month":
trackList.forEach((track) => {
let monthVal = new Date(track[3]).toDateString().split(" ");
let monthYear = monthVal[1] + monthVal[3];
monthYear in sortedObj
? sortedObj[monthYear].push(track)
: (sortedObj[monthYear] = [track]);
});
setSortedList(sortedObj);
break;
case "year":
trackList.forEach((track) => {
let yearVal = new Date(track[3]).toDateString().split(" ");
let year = yearVal[3];
year in sortedObj
? sortedObj[year].push(track)
: (sortedObj[year] = [track]);
});
setSortedList(sortedObj);
break;
}
};
const getUserTracks = (username) => {
fetch(`http://localhost/my/api/${username}`, {
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
})
.then((res) => res.json())
.then(
(result) => {
settracks(result["tracks_played"]);
aggTrackList();
setIsLoaded(true);
},
(error) => {
console.log(error);
setIsLoaded(true);
setError(error);
}
);
};
return (
<div className="App">
<SortMenu
setSort={(selected) => {
setSortby(selected);
}}
/>
<UserForm onSubmit={getUserTracks} />
<div className="trackList">
{isLoaded ? (
Object.entries(sortedList).map(([day, track]) => (
<Card
className="card"
displayMode={sortby}
key={day}
timestamp={day}
content={track}
/>
))
) : (
<div>...</div>
)}
</div>
</div>
);
}
export default App;
The issue I am having is when UserForm is submitted and receives the data. The Card elements do not render unless I update the sortby state by clicking on one of the sortmenu options after the data has loaded. How can I get the data to show automatically after it has been loaded?
I'm creating this project to learn React so if something can be done better or if I am doing things wrong, please let me know.
Thanks.
Edit:
My code on codesandbox - https://codesandbox.io/s/fervent-minsky-bjko8?file=/src/App.js with sample data from my API.
You can do so in two ways:
In a blocking way using useLayoutEffect hook. Refer this.
In a non-blocking way using useEffect hook. Refer this.
1. useLayoutEffect
The thing to note here is that the function passed in the hook is executed first and the component is rendered.
Make the API call inside useLayoutEffect and then set the data once you obtain the response from the API. Where the data can initially be
const [data, setData] = useState(null)
The JSX must appropriately handle all the cases of different responses from the server.
2. useEffect
The thing to note here is that this function runs after the component has been rendered.
Make the API call inside the useEffect hook. Once the data is obtained, set the state variable accordingly.
Here your jsx can be something like
{
data === null ? (
<Loader />
) : data.length > 0 ? (
<Table data={data} />
) : (
<NoDataPlaceholder />
)
}
Here I am assuming the data is a list of objects. but appropriate conditions can be used for any other format. Here while the data is being fetched using the API call made inside useEffect, the user will see a loading animation. Once the data is obtained, the user will be shown the data. In case the data is empty, the user will be show appropriate placeholder message.
For a project I have multiple tables that all need a search bar, I've created a custom hook that filters through the table rows.
Here is my code for my custom filtering hook:
import React from 'react'
import { matchSorter } from 'match-sorter'
const useFilterData = (searchValue, keyList, data) => {
const [filteredList, setFilteredList] = React.useState(data)
React.useEffect(() => {
if (searchValue) {
const filtered = matchSorter(data, searchValue, {
keys: keyList,
})
setFilteredList(filtered)
} else {
setFilteredList(data)
}
}, [searchValue])
return [filteredList]
}
export default useFilterData
Here is a snippet from the Table code
const data = [{}, {}, {}] // this is a list of objects
const columns = [{'field': 'one'}, {'field': 'two'}, {'field': 'three'}]
const columnKeys = columns.map((c) => c.field)
const [searchValue, setSearchValue] = React.useState('')
const [filteredList] = useFilterData(
searchValue,
columnKeys,
data,
)
return (
<div>
<Paper>
<TextField onChange={(e) => setSearchValue(e.target.value)} />
<DataGrid
rows={filteredList}
columns={columns}
/>
</Paper>
</div>
)
}
When filteredList is first rendered it is always an empty list, once you do a search and then clear the search value the data properly displays. Searching works how I want it to except for whats returned in the first render.
I tried using useEffect like so
React.useEffect(() => {
setFilteredList(data)
}, [data])
but that got me stuck in an infinite loop. I also tried being more explicit in my if statement,
if (searchValue !== '')
that also didn't change anything!
I am new to hooks, so i'm hoping i'm missing something simple. Thanks.
EDIT: Still been trying things, I've narrowed down the problem I think
console.log(data)
const [filteredList, setFilteredList] = React.useState(data)
console.log(filteredList)
React.useEffect(() => {
setFilteredList(data)
}, [])
console.log(filteredList)
The first console log correctly returns something, the last two always return an empty list.
This will solve your problem:
React.useEffect(() => {
setFilteredList(data)
}, [])
I have a React component that fetches data using the useEffect hook like so:
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
useEffect(() => {
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
}, [key, options])
return <p>{data}</p>;
}
This runs the hook every time key or options change. However, I'm also caching the data locally, and only want the effect to run when both key AND options change (since for each key/options combination the data will always be the same).
Is there a clean way to depend on the combination of key AND options rather than key OR options using React Hooks?
You can create this sort of logic with useRef(). Consider the following example and sandbox: https://codesandbox.io/s/react-hooks-useeffect-with-multiple-reqs-6ece5
const App = () => {
const [name, setName] = useState();
const [age, setAge] = useState();
const previousValues = useRef({ name, age });
useEffect(() => {
if (
previousValues.current.name !== name &&
previousValues.current.age !== age
) {
//your logic here
console.log(name + " " + age);
console.log(previousValues.current);
//then update the previousValues to be the current values
previousValues.current = { name, age };
}
});
return (
<div>
<input
placeholder="name"
value={name}
onChange={e => setName(e.target.value)}
/>
<input
placeholder="age"
value={age}
onChange={e => setAge(e.target.value)}
/>
</div>
);
};
Workflow:
We create a ref object for the two values we want to keep track of,
in this case its a name and age. The ref object is previousValues.
useEffect is defined but we do not provide it any dependencies.
Instead, we just have it execute whenever there is a state-change to
name or age.
Now inside useEffect we have conditional logic to check whether the
previous/initial values of both name and age are different than
their corresponding state-values. If they are then good we execute
our logic (console.log).
Lastly after executing the logic, update the ref object (previousValues) to the current values (state).
In order to run the effect when both values change, you need to make use of the previous values and compare them within the hook when either key or options change.
You can write a usePrevious hook and compare old and previous state as mentioned in this post:
How to compare oldValues and newValues on React Hooks useEffect?
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
const previous = usePrevious({key, options});
useEffect(() => {
if(previous.key !== key && previous.options !== options) {
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
}
}, [key, options])
return <p>{data}</p>;
}
All provided solutions are perfectly fine, However there are some more complex situation e.g., When useEffect function should be called ONLY when dependency A and B changed while it also depends on C's value.
So I suggest using sequence of useEffects and intermediate States to provide more space for future logics. Implementation of this approach for asked question would be:
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
const [needsUpdate, setNeedsUpdate] = useState(()=>({key:false, option:false}));
useEffect(()=>{
setNeedsUpdate((needsUpdate)=>({...needsUpdate, key:true}));
},[key])
useEffect(()=>{
setNeedsUpdate((needsUpdate)=>({...needsUpdate, options:true}));
},[options])
useEffect(() => {
if (needsUpdate.key && needsUpdate.options){
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
setNeedsUpdate(()=>({key:false, option:false}));
}
}, [needsUpdate, key, options])
return <p>{data}</p>;
}
In this way we can apply almost any logic on our useEffect dependencies, However it has own drawbacks which is few more rendering cycle.
You can create a new custom hook which calls the callback with an argument with index/names of dependencies
const useChangesEffect = (callback, dependencies, dependencyNames = null) => {
const prevValues = useRef(dependencies);
useEffect(() => {
const changes = [];
for (let i = 0; i < prevValues.current.length; i++) {
if (!shallowEqual(prevValues.current[i], dependencies[i])) {
changes.push(dependencyNames ? dependencyNames[i] : i);
}
}
callback(changes);
prevValues.current = dependencies;
}, dependencies);
};
useChangesEffect((changes) => {
if (changes.includes(0)) {
console.log('dep1 changed');
}
if (changes.includes(1)) {
console.log('dep2 changed');
}
}, [dep1, dep2]);
I have built a function component, DepartmentSelect, that uses react-select Async to allow users to select a department from a dropdown. I want to pass the id of the selected department in the props.
I have a working example, but it seems like I have duplicated logic. I passed a promise to the loadOptions react-select prop and I store the options in the state. I have the departmentId of the selected department in the DepartmentSelect props and I am storing the selected value in the state.
//... imports and getOptionLabel removed for brevity
const getOptionValue = (option) => {
return option.id;
};
interface Props {
departmentId: string,
onChange: (number) => void
}
let options = [];
const DepartmentSelect = (props: Props) => {
const [value, setValue] = useState(
options.find(o => o.id == props.departmentId)
);
const [isLoading, setLoading] = useState(true);
const handleChange = (option, action) => {
const id = option && option.id;
setValue(options.find(o => o.id == id));
props.onChange(id);
};
const loadOptions = () => {
return ky.get('/sbm/departments.json')
.then(r => r.json())
.then(json => {
options = json;
setValue(options.find(o => o.id == props.departmentId));
setLoading(false);
return json;
});
};
return (
<AsyncSelect
defaultOptions
getOptionValue={getOptionValue}
loadOptions={loadOptions}
onChange={handleChange}
value={value}
/>
);
};
export default DepartmentSelect;
The code is working. I have removed a few irrelevant lines to make it shorter. Is there a way I can have the same functionality without storing the options and value in the state?
This question is very similar to
How can I work with just values in react-select onChange event and value prop?
In the above question, the options are passed in as a prop, so that they can get the selected prop without having to check if the options have been loaded from the server first. Would it be better to load the options separately and use a non-async select?
Edit
It would be nice if I could pass props.departmentId as the value prop to Async, but props.departmentId is a string. The value prop of Async requires one of the options, which are in the format
{
departmentId: string,
bill_cd: string,
name: string
}