I tried to list search result onTyping on the autocomplete using useSearch hook.
I have two problems here.
loads searchdata to options only after a rerender(does not mutate)
encountered multiple child with same key
enter image description here
only adds searchdata to options after a rerender
enter image description here
Here is the image of the search result after a rerender
enter image description here
SearchAutocomplete.jsx
import * as React from 'react'
import TextField from '#mui/material/TextField'
import Autocomplete from '#mui/material/Autocomplete'
import useSearch from 'data/search/useSearch'
import { useHistory } from 'react-router-dom'
export default function SearchAutocomplete() {
const [open, setOpen] = React.useState(false)
const [options, setOptions] = React.useState([])
const [inputValue, setInputvalue] = React.useState([])
const [loading, setLoading] = React.useState(false)
const { isLoadingInitialData, searchdata } = useSearch(inputValue)
const history = useHistory()
console.log(searchdata, 'searchdata')
console.log(opptions, 'options')
React.useEffect(() => {
if (isLoadingInitialData) {
setLoading(true)
} else {
setOptions([...searchdata?.[0]?.data])
}
}, [loading, inputValue])
React.useEffect(() => {
if (!open) {
setLoading(false)
setOptions([])
}
}, [open])
return (
<Autocomplete
id="asynchronous-demo"
sx={{ width: 300 }}
open={open}
onOpen={() => {
setOpen(true)
}}
onClose={() => {
setOpen(false)
}}
filterOptions={(x) => x}
isOptionEqualToValue={(option, value) => option?.title === value?.title}
getOptionLabel={(option) => (option?.title ? option?.title : '')}
options={options}
loading={loading}
onChange={(event, value) => {
history.push(value?.route)
}}
onInputChange={(event, newInputValue) => {
setLoading(true)
setInputvalue(newInputValue)
}}
renderOption={(props, option) => {
return (
<li {...props} key={option.id}>
{option.title}
</li>
)
}}
renderInput={(params) => (
<TextField
{...params}
label="Search ..."
InputProps={{
...params.InputProps,
endAdornment: <React.Fragment>{params.InputProps.endAdornment}</React.Fragment>
}}
/>
)}
/>
)
}
useSearch.js
export default function useSearch(searchValue) {
let url = urls.search
const search = searchValue || ''
const getKey = (pageIndex) => {
return `${url}?search=${search}`
}
const { data, error, size, setSize, mutate } = useSWRInfinite(getKey, fetcher, {
shouldRetryOnError: false
})
const searchdata = data ? [].concat(...data) : []
const isLoadingInitialData = !data && !error
const isLoadingMore =
isLoadingInitialData || (size > 0 && data && typeof data[size - 1] === 'undefined')
const isEmpty = data?.[0]?.data?.length === 0
return {
isLoadingInitialData,
isLoadingMore,
error,
searchdata,
mutate,
setSize,
size
}
}
I tried to get the search results as options on MUI Autocomplete. Created a state.
const [inputValue, setInputvalue] = React.useState([])
and set the value on onInputChange
onInputChange={(event, newInputValue) => {
setInputvalue(newInputValue)
}}
After getting the value on searchdata
const { isLoadingInitialData, searchdata } = useSearch(inputValue)
only adds data on rerender
React.useEffect(() => {
if (isLoadingInitialData) {
setLoading(true)
} else {
setOptions([...searchdata?.[0]?.data])
}
}, [loading, inputValue])
How do I set searchdata to options without rerendering ?
How do I fix multiple child with same key error ?
Related
I am using React-Bootstrap-TypeAhead's latest version in my React project. The main goal is to display the options menu when the user types. The menu is displayed when using the default input component but once I use the render input method for customization the options menu stops showing:
working example
import React, { useState } from 'react';
import { AsyncTypeahead } from 'react-bootstrap-typeahead';
/* example-start */
const BasicExample = ({ key, label }) => {
const [singleSelections, setSingleSelections] = useState([]);
const [multiSelections, setMultiSelections] = useState([]);
const [query, setQuery] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [options, setOptions] = useState([]);
const PER_PAGE = 50;
const SEARCH_URI = 'https://api.github.com/search/users';
function makeAndHandleRequest(query, page = 1) {
return fetch(`${SEARCH_URI}?q=${query}+in:login&page=${page}&per_page=50`)
.then((resp) => resp.json())
.then(({ items, total_count }) => {
/* eslint-disable-line camelcase */
const options = items.map((i) => ({
avatar_url: i.avatar_url,
id: i.id,
login: i.login,
}));
return { options, total_count };
})
.catch((err) => console.log(err));
}
const _handleInputChange = (query) => {
setQuery(query);
};
const _handlePagination = (e, shownResults) => {
const { query } = this.state;
const cachedQuery = this._cache[query];
// Don't make another request if:
// - the cached results exceed the shown results
// - we've already fetched all possible results
if (cachedQuery.options.length > shownResults || cachedQuery.options.length === cachedQuery.total_count) {
return;
}
setIsLoading(true);
const page = cachedQuery.page + 1;
makeAndHandleRequest(query, page).then((resp) => {
const options = cachedQuery.options.concat(resp.options);
// this._cache[query] = { ...cachedQuery, options, page };
setIsLoading(false);
setOptions(options);
});
};
const _handleSearch = (query) => {
setIsLoading(true);
makeAndHandleRequest(query).then((resp) => {
setIsLoading(true);
setOptions(resp?.options || []);
});
};
return (
<>
<AsyncTypeahead
{...{ query, isLoading, options }}
id="async-pagination-example"
labelKey="login"
maxResults={PER_PAGE - 1}
minLength={2}
onInputChange={_handleInputChange}
onPaginate={_handlePagination}
onSearch={_handleSearch}
renderInput={({ inputRef, referenceElementRef, ...inputProps }) => (
<div className="form-group h-64">
<label>Job Category</label>
<div className="input-group">
<input
type="text"
{...inputProps}
ref={(input) => {
inputRef(input);
// referenceElementRef(input);
}}
className="form-control"
placeholder=""
/>
</div>
</div>
)}
paginate
placeholder="Search for a Github user..."
renderMenuItemChildren={(option) => (
<div key={option.id}>
<img
alt={option.login}
src={option.avatar_url}
style={{
height: '24px',
marginRight: '10px',
width: '24px',
}}
/>
<span>{option.login}</span>
</div>
)}
useCache={false}
/>
</>
);
};
/* example-end */
export default BasicExample;
The reason you're not seeing any results rendered is that _handleInputChange is triggering a re-render and resetting the debounced onSearch handler before it can fire.
You can wrap _handleSearch with useCallback to fix that:
const _handleSearch = useCallback((query) => {
setIsLoading(true);
makeAndHandleRequest(query).then((resp) => {
setIsLoading(false);
setOptions(resp?.options || []);
});
}, []);
import React, { useState, useEffect, Fragment } from "react";
import Button from "../../../Resources/Forms/Button";
import Switch from "../../../Resources/Forms/Switch";
import { POST } from "../../../Utils/api";
import Radio from "../../../Resources/Forms/Radio";
import withAppData from "../../../HOC/withAppData";
const InventorySettings = (props) => {
const [state, setState] = useState({});
const [isSaving, setIsSaving] = useState();
const [isStockRequestChecked, setIsStockRequestChecked] = useState(false);
const getStatus = id => {
return props.context.isSettingsActivated(id) ? 1 : 0;
};
const setBusinessSettings = async () => {
const defaultSettings = [
{ state: "enableStockRequest", id: 53 },
{ state: "connectWarehousePickStock", id: 52 },
{ state: "approveRequestOtp", id: 51 },
{ state: "approveRequestManually", id: 50 }
];
for (const setting of defaultSettings) {
await setState({ [setting.state]: getStatus(setting.id) });
}
};
function chooseApprovalMethod(methodType) {
const currentValue = state[methodType];
setState({[methodType]
: currentValue === 1 ? 0: 1})
}
async function saveApprovalMethod() {
setIsSaving(true)
const approvalSettings = [{text:"approvalRequestManually", id: 51}, {text:"approveRequestOtp", id: 50}]
for(const el of approvalSettings) {
const currentValue = state[el.text];
const data = {
settingId: el.id,
status: currentValue
}
await POST(`Common/AddBusinessSetting`, data);
}
setIsSaving(false);
props.context.getBusinessSettings();
}
const updateBasicSettings = async (id, key) => {
setState({ [key]: !state[key] ? 1 : 0 });
const data = {
SettingId: id,
Status: state[key],
};
await POST(`Common/AddBusinessSetting`, data);
props.context.getBusinessSettings();
};
useEffect(() => {
setBusinessSettings();
}, []);
return (
<Fragment>
<div className="basic-settings-section">
<Switch
label={"Connect Warehouse Stock to pick stock"}
light={true}
checked={state && state.connectWarehousePickStock === 1}
onChange={() => updateBasicSettings(52, "connectWarehousePickStock")}
></Switch>
</div>
<div className="basic-settings-section">
<Switch
label={"Stock Request"}
light={true}
checked={isStockRequestChecked}
onChange={() => setIsStockRequestChecked(!isStockRequestChecked)}
></Switch>
{isStockRequestChecked && (
<div className="basic-settings-plan-generate">
<div
className="form__label"
style={{ padding: "2px", marginBottom: "20px" }}
>
<p>Please choose an approval method</p>
</div>
<Radio
label={"Manual Approval"}
name="approval"
value="50"
id="50"
checked={state && state.approveRequestManually === 1}
// onChange={() => (chooseApprovalMethod)}
/>
<Radio
label={"OTP Approval"}
name="approval"
value="51"
id="51"
checked={state && state.approveRequestOtp === 1}
// onChange={() => (chooseApprovalMethod)}
/>
<div className="password-settings-btn"
// onClick={props.context.showToast}
>
<Button
type={"outline"}
size={"medium"}
text={"Save"}
disabled={!state.approveRequestOtp && !state.approveRequestManually}
withMargin={false}
loading={isSaving}
onClick={saveApprovalMethod}
></Button>
</div>
</div>
)}
</div>
</Fragment>
);
}
export default withAppData(InventorySettings);
I added the chooseApprovalMethod function to the radio buttons but still I wasn't getting it well. So I had to call there state using state.text is equal to 1. Please help me out I don't think I know what I'm doing anymore.
Please above are my code, the radio buttons aren't checking or highlighting, so I want them to be checked when clicked on, and I want there ids to be saved when clicking on the save button.
So please guys help me out, as I don't understand it anymore.
I want the users to be able to select multiple tags while also allowing them to add a tag if it does not exist, the examples on the material UI documentation work on the freeSolo option which works on string / object values as options whereas when we use multiple, that changes to an array
How do I implement a multiple creatable with material-ui?
My code:
// Fetch Adding tag list
const [listOpen, setListOpen] = useState(false);
const [options, setOptions] = useState<Tag[]>([]);
const loadingTags = listOpen && options.length === 0;
useEffect(() => {
let active = true;
if (!loadingTags) {
return undefined;
}
(async () => {
try {
const response = await getAllTagsForUser();
if (active) {
setOptions(response.data);
}
} catch (error) {
console.log(error);
}
})();
return () => {
active = false;
};
}, [loadingTags]);
useEffect(() => {
if (!listOpen) {
setOptions([]);
}
}, [listOpen]);
<Autocomplete
multiple
id="tags"
open={listOpen}
onOpen={() => {
setListOpen(true);
}}
onClose={() => {
setListOpen(false);
}}
options={options}
disableCloseOnSelect
getOptionLabel={(option) => option?.name || ""}
defaultValue={
contact?.tags?.map((element) => {
return { name: element };
}) || undefined
}
renderOption={(option, { selected }) => (
<React.Fragment>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
{option.name}
</React.Fragment>
)}
style={{ width: 500 }}
renderInput={(params) => (
<TextField {...params} variant="outlined" label="Tags" />
)}
/>;
This is just fetching tags from the server and showing them as options, I understand that to be able to allow adding more, I would need to add filterOptions and onChange but, can someone please provide an example on how to deal with array there?
I know this isn't an quick answer but may someone else could use it. Found this Question buy searching an solution. Didn't find one so I tryed myself and this is what I Created and seems it works.
Based on the original Docs https://mui.com/components/autocomplete/#creatable
Complete example:
import React, { useEffect, useState } from "react";
//Components
import TextField from "#mui/material/TextField";
import Autocomplete, { createFilterOptions } from "#mui/material/Autocomplete";
//Icons
const filter = createFilterOptions();
export default function AutocompleteTagsCreate() {
const [selected, setSelected] = useState([])
const [options, setOptions] = useState([]);
useEffect(() => {
setOptions(data);
}, [])
return (
<Autocomplete
value={selected}
multiple
onChange={(event, newValue, reason, details) => {
let valueList = selected;
if (details.option.create && reason !== 'removeOption') {
valueList.push({ id: undefined, name: details.option.name, create: details.option.create });
setSelected(valueList);
}
else {
setSelected(newValue);
}
}}
filterSelectedOptions
filterOptions={(options, params) => {
const filtered = filter(options, params);
const { inputValue } = params;
// Suggest the creation of a new value
const isExisting = options.some((option) => inputValue === option.name);
if (inputValue !== '' && !isExisting) {
filtered.push({
name: inputValue,
label: `Add "${inputValue}"`,
create: true
});
}
return filtered;
}}
selectOnFocus
clearOnBlur
handleHomeEndKeys
id="tags-Create"
options={options}
getOptionLabel={(option) => {
// Value selected with enter, right from the input
if (typeof option === 'string') {
return option;
}
// Add "xxx" option created dynamically
if (option.label) {
return option.name;
}
// Regular option
return option.name;
}}
renderOption={(props, option) => <li {...props}>{option.create ? option.label : option.name}</li>}
freeSolo
renderInput={(params) => (
<TextField {...params} label="Tags" />
)}
/>
);
}
const data = [
{
id: 1,
name: 'Tag1'
},
{
id: 2,
name: 'Tag2'
},
{
id: 3,
name: 'Tag3'
},
{
id: 4,
name: 'Tag4'
},
]
how I can update price value when update quantity value automatically ??
page design
interface ui
print values on the console:
print values on the console:
This sentence needs to be modified
{quantity[initQ] == 1 ? data.price : initP[initQ]}
i use setState to save multiple values
export default function Contaner({ setPressed, getPressed }) {
const [products, setProducts] = useState([]);
const [layout, setLayout] = useState('list');
let initQ = 1;
const [initP,setInitP] = useState({ [initQ]: 1 }) ;
const [quantity, setQuantity] = useState({ [initQ]: 1 });
function checkQuantity(e, data) {
if (e.value <= data.quantity) {
initQ = data.name;
setQuantity({ ...quantity, [data.name]: e.value});
setInitP( { ...quantity, [data.name]: data.price * e.value});
console.log(initP );
setCart(current => [...current, data.name]);
}
else {
showError();
}
}
const renderListItem = (data) => {
return (
<div style={{ display: "flex" }}>
<button className="button_color" onClick={() => removeItem(data)}>
<i className="pi pi-trash"></i>
</button>
<h6>{quantity[initQ] == 1 ? data.price : initP[initQ] }</h6>
<InputNumber id="stacked" showButtons min={1} value={quantity[initQ]}
onValueChange={(e) => checkQuantity(e, data)} />
<InputText disabled={true} value={"₪ " + data.price} />
<h6>{data.name}</h6>
</div>
);
}
const itemTemplate = (product, layout) => {
if (!product) {
return <></>;
}
if (layout === 'list') {
return renderListItem(product);
}
}
return(
<DataView value={products} layout={layout} itemTemplate={itemTemplate} rows={1} />
);
}
I want to add a debounce function to not launch the search api for every character:
export const debounce = <F extends ((...args: any) => any)>(
func: F,
waitFor: number,
) => {
let timeout: number = 0;
const debounced = (...args: any) => {
clearTimeout(timeout);
setTimeout(() => func(...args), waitFor);
};
return debounced as (...args: Parameters<F>) => ReturnType<F>;
};
Search.tsx:
import React, { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getAllTasks } from "./taskSlice";
import { ReturnedTask } from "../../api/tasks/index";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
import { AppState } from "../../app/rootReducer";
import { debounce } from "../../app/utils";
const SearchTask = () => {
const dispatch = useDispatch();
const [open, setOpen] = React.useState(false);
const debounceOnChange = useCallback(debounce(handleChange, 400), []);
const { tasks, userId } = useSelector((state: AppState) => ({
tasks: state.task.tasks,
userId: state.authentication.user.userId,
}));
const options = tasks.length
? tasks.map((task: ReturnedTask) => ({ title: task.title }))
: [];
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
function handleChange(
e: React.ChangeEvent<{}>,
value: string,
) {
const queryParams = [`userId=${userId}`];
if (value.length) {
console.log(value);
queryParams.push(`title=${value}`);
dispatch(getAllTasks(queryParams));
}
}
return (
<Autocomplete
style={{ width: 300 }}
open={open}
onOpen={handleOpen}
onClose={handleClose}
onInputChange={(e: React.ChangeEvent<{}>, value: string) =>
debounceOnChange(e, value)}
getOptionSelected={(option, value) => option.title === value.title}
getOptionLabel={(option) => option.title}
options={options}
renderInput={(params) => (
<TextField
{...params}
label="Search Tasks"
variant="outlined"
InputProps={{ ...params.InputProps, type: "search" }}
/>
)}
/>
);
};
export default SearchTask;
The actual behavior of the component is launching the api for every character typed. I want to launch the api only after an amount of time. How can I fix it?
As #Jayce444 mentioned in the comment, the the context was not preserved between every call and the function.
export const debounce = <F extends ((...args: any) => any)>(
func: F,
waitFor: number,
) => {
let timeout: NodeJS.Timeout;
return (...args: any) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), waitFor);
};
};