I have a pretty complicated component and now I am trying to implement a search where the user can type and it filters the results.
// query
const GET_ACCOUNTS = gql`
query accounts{
accounts{
id
name
status
company{
id
name
}
}
}
`;
// get query
const { loading } = useQuery(GET_ACCOUNTS, {
fetchPolicy: "no-cache",
skip: userType !== 'OS_ADMIN',
onCompleted: setSearchResults
});
// example of query result (more than 1)
{
"accounts": [
{
"id": "5deed7df947204960f286010",
"name": "Acme Test Account",
"status": "active",
"company": {
"id": "5de84532ce5373afe23a05c8",
"name": "Acme Inc.",
"__typename": "Company"
},
"__typename": "Account"
},
]
}
// states
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState([]);
// code to render
<FormControl fullWidth>
<InputLabel htmlFor="seach">Search for accounts</InputLabel>
<Input
id="search"
aria-describedby="Search for accounts"
startAdornment={<InputAdornment position="start"><SearchIcon /></InputAdornment>}
value={searchTerm}
onChange={handleChange}
/>
</FormControl>
{searchResults && searchResults.accounts &&
searchResults.accounts.map(c => {
return (
<>
<ListItem
dense
button
className={classnames({ [classes.selectedAccountContext]: c.id === accountContextId })}
key={c.id}
onClick={() => accountClicked(c.id)}
>
<ListItemText
primary={c.name}
secondary={
<>
<span>{c.company.name}</span>
<span className="d-flex align-items-center top-margin-tiny">
<Badge
color={c.status === 'active' ? "success" : "danger"}
style={{ marginBottom: 0 }}
>
{c.status.replace(/^\w/, c => c.toUpperCase())}
</Badge>
<span className='ml-auto'>
<SvgIcon><path d={mdiMapMarkerRadius} /></SvgIcon>
<SMARTCompanyIcon />
</span>
</span>
</>
}
/>
</ListItem>
</>
)
})
}
// useEffect
useEffect(() => {
if (searchTerm) {
const results = searchResults.accounts.filter((c => c.name.toLowerCase().includes(searchTerm)))
setSearchResults(results)
}
}, [searchTerm])
The issue is when I start typing in my search field, I am looking at my searchResults and it gets filtered when I type in one character, but when I type the next one it breaks.
TypeError: Cannot read property 'filter' of undefined
Also it does not render on the view even when I have typed one letter.
Based on your data, the initial value of searchResults is a dictionary with accounts key. But when you update it in the useEffect part, it changes to a list:
useEffect(() => {
if (searchTerm) {
const results = searchResults.accounts.filter((c => c.name.toLowerCase().includes(searchTerm)))
// This changes the value of searchResults to an array
setSearchResults(results)
}
}, [searchTerm])
When the setSearchResults was called inside the useEffect, the value of searchResults changes from an object to an array:
from this:
searchResults = {
accounts: [
...
]
}
to this:
searchResults = [
...
]
That is why it raises TypeError: Cannot read property 'filter' of undefined after the first search since there is no accounts key anymore.
To fix this, you need to be consistent in the data type of your searchResults, it would be better to make it as a List in the first place. You can do this in the onCompleted part:
const { loading } = useQuery(GET_ACCOUNTS, {
fetchPolicy: "no-cache",
skip: userType !== 'OS_ADMIN',
onCompleted: (data) => setSearchResults(data.accounts || [])
});
Notice that we set searchResults to the accounts value. After that, you also need the way on how you access searchResults
{searchResults &&
searchResults.map(c => {
return (
...renderhere
)
})
}
And your useEffect will be like this:
useEffect(() => {
if (searchTerm) {
const results = searchResults.filter((c => c.name.toLowerCase().includes(searchTerm)))
setSearchResults(results)
}
}, [searchTerm])
TIP:
You may want to rename your searchResults into accounts to make it clearer. Take note also that after the first search, your options will become limited to the previous search result, so you may also want to store all of the accounts in a different variable:
const [allAccounts, setAllAccounts] = useState([])
const [searchedAccounts, setSearchedAccounts] = useState([])
// useQuery
const { loading } = useQuery(GET_ACCOUNTS, {
fetchPolicy: "no-cache",
skip: userType !== 'OS_ADMIN',
onCompleted: (data) => {
setAllAccounts(data.accounts || [])
setSearchedAccounts(data.accounts || [])
}
});
// useEffect
useEffect(() => {
if (searchTerm) {
// Notice we always search from allAccounts
const results = allAccounts.filter((c => c.name.toLowerCase().includes(searchTerm)))
setSearchedAccounts(results)
}
}, [searchTerm])
Related
Stackoverflow
problem
I have separate components that house Tiptap Editor tables. At first I had a save button for each Child Component which worked fine, but was not user friendly. I want to have a unified save button that will iterate through each child Table component and funnel all their editor.getJSON() data into an array of sections for the single doc object . Then finish it off by saving the whole object to PouchDB
What did I try?
link to the repo β wchorski/Next-Planner: a CRM for planning events built on NextJS (github.com)
Try #1
I tried to use the useRef hook and the useImperativeHandle to call and return the editor.getJSON(). But working with an Array Ref went over my head. I'll post some code of what I was going for
// Parent.jsx
const childrenRef = useRef([]);
childrenRef.current = []
const handleRef = (el) => {
if(el && !childrenRef.current.includes(el)){
childrenRef.current.push(el)
}
}
useEffect(() =>{
childrenRef.current[0].childFunction1() // I know this doesn't work, because this is where I gave up
})
// Child.jsx
useImperativeHandle(ref, () => ({
childFunction1() {
console.log('child function 1 called');
},
childFunction2() {
console.log('child function 2 called');
},
}))
Try #2
I set a state counter and passed it down as a prop to the Child Component . Then I update the counter to trigger a child function
// Parent.jsx
export const Planner = ({id, doc, rev, getById, handleSave, db, alive, error}) => {
const [saveCount, setSaveCount] = useState(0)
const handleUpdate = () =>{
setSaveCount(prev => prev + 1)
}
const isSections = () => {
if(sectionsState[0]) handleSave(sectionsState)
if(sectionsState[0] === undefined) console.log('sec 0 is undefined', sectionsState)
}
function updateSections(newSec) {
setsectionsState(prev => {
const newState = sectionsState.map(obj => {
if(!obj) return
if (obj.header === newSec.header) {
return {...obj, ...newSec}
}
// ποΈ otherwise return object as is
return obj;
});
console.log('newState', newState);
return newState;
});
}
useEffect(() => {
setsectionsState(doc.sections)
}, [doc])
return (<>
<button
title='save'
className='save'
onPointerUp={handleUpdate}>
Save to State <FiSave />
</button>
<button
style={{right: "0", width: 'auto'}}
title='save'
className='save'
onClick={isSections}>
Save to DB <FiSave />
</button>
{doc.sections.map((sec, i) => {
if(!sec) return
return (
<TiptapTable
key={i}
id={id}
rev={doc.rev}
getById={getById}
updateSections={updateSections}
saveCount={saveCount}
section={sec}
db={db}
alive={alive}
error={error}
/>
)
})}
</>)
// Child.jsx
export const TiptapTable = ((props, ref) => {
const {id, section, updateSections, saveCount} = props
const [currTimeStart, setTimeStart] = useState()
const [defTemplate, setdefTemplate] = useState('<p>loading<p>')
const [isLoaded, setIsLoaded] = useState(false)
const [notesState, setnotesState] = useState('')
const editor = useEditor({
extensions: [
History,
Document,
Paragraph,
Text,
Gapcursor,
Table.configure({
resizable: true,
}),
TableRow.extend({
content: '(tableCell | tableHeader)*',
}),
TableHeader,
TableCell,
],
// i wish it was this easy
content: (section.data) ? section.data : defTemplate,
}, [])
const pickTemplate = async (name) => {
try{
const res = await fetch(`/templates/${name}.json`,{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json()
setIsLoaded(true)
setdefTemplate(data)
console.log('defTemplate, ', defTemplate);
// return data
} catch (err){
console.warn('template error: ', err);
}
}
function saveData(){
console.log(' **** SAVE MEEEE ', section.header);
try{
const newSection = {
header: section.header,
timeStart: currTimeStart,
notes: notesState,
data: editor.getJSON(),
}
updateSections(newSection)
} catch (err){
console.warn('table update error: ', id, err);
}
}
useEffect(() => {
// ποΈ don't run on initial render
if (saveCount !== 0) saveData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
useEffect(() => {
setTimeStart(section.timeStart)
setnotesState(section.notes)
if(!section.data) pickTemplate(section.header).catch(console.warn)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, section, isLoaded])
useEffect(() => {
if (editor && !editor.isDestroyed) {
if(section.data) editor.chain().focus().setContent(section.data).run()
if(!section.data) editor.chain().focus().setContent(defTemplate).run()
setIsLoaded(true)
}
}, [section, defTemplate, editor]);
if (!editor) {
return null
}
return isLoaded ? (<>
<StyledTableEditor>
<div className="title">
<input type="time" label='Start Time' className='time'
onChange={(e) => setTimeStart(e.target.value)}
defaultValue={currTimeStart}
/>
<h2>{section.header}</h2>
</div>
<EditorContent editor={editor} className="tiptap-table" ></EditorContent>
// ... non relavent editor controls
<button
title='save'
className='save2'
onPointerUp={() => saveData()}>
Save <FiSave />
</button>
</div>
</nav>
</StyledTableEditor>
</>)
: null
})
TiptapTable.displayName = 'MyTiptapTable';
What I Expected
What I expected was the parent state to update in place, but instead it overwrites the previous tables. Also, once it writes to PouchDB it doesn't write a single piece of new data, just resolved back to the previous, yet with an updated _rev revision number.
In theory I think i'd prefer the useRef hook with useImperativeHandle to pass up the data from child to parent.
It looks like this question is similar but doesn't programmatically comb through the children
I realize I could have asked a more refined question, but instead of starting a new question I'll just answer my own question from what I've learned.
The problem being
I wasn't utilizing React's setState hook as I iterated and updated the main Doc Object
Thanks to this article for helping me through this problem.
// Parent.jsx
import React, {useState} from 'react'
import { Child } from '../components/Child'
export const Parent = () => {
const masterDoc = {
_id: "123",
date: "2023-12-1",
sections: [
{header: 'green', status: 'old'},
{header: 'cyan', status: 'old'},
{header: 'purple', status: 'old'},
]
}
const [saveCount, setSaveCount] = useState(0)
const [sectionsState, setsectionsState] = useState(masterDoc.sections)
function updateSections(inputObj) {
setsectionsState(prev => {
const newState = prev.map(obj => {
// ποΈ if id equals 2, update country property
if (obj.header === inputObj.header)
return {...obj, ...inputObj}
return obj;
});
return newState;
});
}
return (<>
<h1>Parent</h1>
{sectionsState.map((sec, i) => {
if(!sec) return
return (
<Child
key={i}
section={sec}
updateSections={updateSections}
saveCount={saveCount}
/>
)
})}
<button
onClick={() => setSaveCount(prev => prev + 1)}
>State dependant update {saveCount}</button>
</>)
}
// Child.jsx
import React, {useEffect, useState, forwardRef, useImperativeHandle} from 'react'
export const Child = forwardRef((props, ref) => {
const {section, updateSections, saveCount} = props
const [statusState, setStatusState] = useState(section.status)
function modData() {
const obj = {
header: section.header,
status: statusState
}
updateSections(obj)
}
useEffect(() => {
// ποΈ don't run on initial render
if (saveCount !== 0) modData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
return (<>
<span style={{color: section.header}}>
header: {section.header}
</span>
<span>status: {section.status}</span>
<input
defaultValue={section.status}
onChange={(e) => setStatusState(e.target.value)}
/>
________________________________________
</>)
})
Child.displayName = 'MyChild';
I set state(id) on button click and i want to use that data(id) in that state, it just showing undefined in the first click
[State Initialization]
const [id,setId] = useState("");
[Onclick of Button]
`render: (record) => (
<div>
<Button
className="details-btn-approve"
onClick={()=>{
handleApprove(record)
}}
>
Approve
</Button>
)`
[useEffect and calling on click action]
`
useEffect(()=>{
},[id]);
const handleApprove = async (record) => {
setId(record.ID);
console.log(id);
}
`
i was expecting the id to be changing every time i click button
here is the complete Code
`
import {useEffect, useState} from "react";
import {Button, Card, Input, Modal, Table} from "antd";
import axios from "axios";
import {endPoints} from "./ApiEndPoints";
function Approval() {
const [isLoading, setIsLoading] = useState(false);
const [dataSource, setDataSource] = useState([]);
const [searchedText,setSearchedText] = useState("");
const [id,setId] = useState("");
useEffect(()=>{
ViewUnApproved();
},[setDataSource])
const ViewUnApproved = async () =>{
setIsLoading(true);
const {data: response} = await axios.get(
endPoints.pendingApprovalAsset
);
if (response["status"] === 100){
setIsLoading(false)
setDataSource(response["pending_approval"]);
} else if(response["status"] === 104){
console.log('no data π’π’π’')
}
}
useEffect(()=>{
},[id])
const handleApprove = async (record) => {
setId(record.ID);
const data = {
ASSET_ID: id,
}
const {data: response } = await axios.post(
endPoints.approve,
data
);
if((response["status"] = 100)){
setIsLoading(false);
console.log(response.to_be_approved);
}
}
console.log(id);
const columns = [
{
title: "Asset",
dataIndex: "DESCRIPTION",
key: "name",
filteredValue:[searchedText],
onFilter:(value,record)=>{
return String(record.DESCRIPTION)
.toLowerCase()
.includes(value.toLowerCase()) ||
String(record.TAG_NUMBER)
.toLowerCase()
.includes(value.toLowerCase()) ||
String(record.BUILDING_NAME)
.toLowerCase()
.includes(value.toLowerCase()) ||
String(record.STATUS)
.toLowerCase()
.includes(value.toLowerCase()) ||
String(record.CONDITION)
.toLowerCase()
.includes(value.toLowerCase())
}
},
{
title: "Tag",
dataIndex: "TAG_NUMBER",
key: "tag",
},
{
title: "Location",
dataIndex: "BUILDING_NAME",
key: "location",
},
{
title: "Status",
dataIndex: "STATUS",
key: "status",
},
{
title: "Condition",
dataIndex: "CONDITION",
key: "condition",
},
{
title: "Action",
key: "",
render: (record) => (
<div>
<Button
className="details-btn-approve"
onClick={()=>{
handleApprove(record)
}}
>
Approve
</Button>
<Button
className="details-btn-reject"
id={record.ID}
type="danger"
onClick={() => {
//showModal(record);
}}
>
Reject
</Button>
</div>
),
},
];
return (
<>
<Card
title="LIST OF ASSETS TO BE APPROVED"
bordered={false}
className="table-card"
>
<Input.Search className='search-input'
placeholder ="Search here ..."
onSearch={(value)=>{
setSearchedText(value)
}}
onChange = {(e)=>{
setSearchedText(e.target.value)
}}
/>
<Table
loading={isLoading}
columns={columns}
dataSource={dataSource}
pagination={{ pageSize: 10 }}
/>
</Card>
</>
);
}
export default Approval;
`
Inside your handleApprove, method updating the state of id will only update it in the next rerender.This is usually fast enough if you need it somewhere else in the code (e.g. the code inside return()). However if you need it immediately in handleApprove you should use the value you've calculated.
const handleApprove = async (record) => {
const newId = record.ID;
setId(newId);
console.log(newId);
//do whatever you want with newId
}
state updates cause a re-render as mentioned in this answer. However the state only has the new value after the re-render, whereas your console log gets called before that re-render happens. if you wanted to just do some logic with a value you already know when you are setting that state, then just use the value you know!
Using console.log() inside your handleApprove() function won't display the last value of the ID. Indeed, setId() is an async function so console.log() can be called even if setId() is not done yet. Move your console.log() out of the function and it will display the last id.
Furthermore, you're using class component that are no longer recommended by React. Here is a proper typescript code example: sandbox. If you're not using Typescript, just get rid of all the types and it will be ok for basic JS.
In your example, you're using a <Button /> component. This usage means you should use a callback (doc here) to send the click event to the parent component. The called function should be a useCallback() function (doc here).
How can I retrieve the dishId selected from my options react - select that shows me thedishType in order to send them my parent component FormRender and to the backend.
My first dropdown shows me: Menu1, Menu2...
My second one: type2...
So if I click ontype4, how can I store the related dishId(here = 4). I can click on several values i.e: type2 andtype3.
How do I keep the dish ids i.e : 2 and 3 and send them to my FormRender parent
Menus(first page of my multi - step form):
export default function Menus() {
const [selectionMenus, setSelectionMenus] = useState({});
const [selectionDishes, setSelectionDishes] = useState({});
const [menus, setMenus] = useState([])
const [date, setDate] = useState('')
useEffect(() => {
axios
.post(url)
.then((res) => {
console.log(res);
setMenus(res.data.menus);
})
.catch((err) => {
console.log(err);
});
}, []);
const names = menus?.map(item => {
return {
label: item.menuId,
value: item.name
}
})
const types = menus?.flatMap(item => {
return item.dishes.map(d => ({
label: d.dishId,
value: d.dishType
}))
})
const handle = (e) => {
if (e?.target?.id === undefined) return setInfo(e);
if (e?.target?.id === undefined) return setSelectionMenus(e);
if (e?.target?.id === undefined) return setSelectionDishes(e);
switch (e.target.id) {
case "date":
setDate(e.target.value);
break;
...
default:
}
}
};
return (
<>
<form>
<div>My menus</div>
<label>
Dishes :
<Dropdown
options={names}
value={selectionMenus}
setValue={setSelectionMenus}
isMulti={true}
/>
</label>
<label>
<Dropdown
options={types}
value={selectionDishes}
setValue={setSelectionDishes}
isMulti={true}
/>
</label>
<label>
Date:
<div>
<input
type="date"
name='date'
value={date}
onChange={handle}
id="date"
/>
</div>
</label>
...
</form>
<div>
<button onClick={() => nextPage({ selectionDishes, selectionMenus, date })}>Next</button>
</div>
</>
);
}
Here the parent Component FormRender that is supposed to retrieve the values of all dishId selected and send them to the backend:
export default function FormRender() {
const [currentStep, setCurrentStep] = useState(0);
const [info, setInfo] = useState();
const [user, setUser] = useState();
const headers = ["Menus", "Details", "Final"];
const steps = [
<Menus
nextPage={(menu) => {
setInfo(menu);
setCurrentStep((s) => s + 1);
}}
/>,
<Details
backPage={() => setCurrentStep((s) => s - 1)}
nextPage={setUser}
/>,
<Final />
];
useEffect(() => {
if (info === undefined || user === undefined) return;
const data = {
date: info.date,
id: //list of dishId selected but don't know how to do that??
};
}, [info, user]);
return (
<div>
<div>
<Stepper steps={headers} currentStep={currentStep} />
<div >{steps[currentStep]}</div>
</div>
</div>
);
}
Dropdown:
export default function Dropdown({ value, setValue, style, options, styleSelect, isMulti = false }) {
function change(option) {
setValue(option.value);
}
return (
<div onClick={(e) => e.preventDefault()}>
{value && isMulti === false ? (
<Tag
selected={value}
setSelected={setValue}
styleSelect={styleSelect}
/>
) : (
<Select
value={value}
onChange={change}
options={options}
isMulti={isMulti}
/>
)}
</div>
);
}
Here my json from my api:
{
"menus": [
{
"menuId": 1,
"name": "Menu1",
"Description": "Descritption1",
"dishes": [
{
"dishId": 2,
"dishType": "type2"
},
{
"dishId": 3,
"dishType": "type3"
},
{
"dishId": 4,
"dishType": "type4"
}
]
},
...
]
}
You already store the selected values inside the selectionMenus and selectionDishes states. So, if you want to send them to the parent FormRender component you can instead create those two states inside that component like this:
export default function FormRender() {
const [selectionMenus, setSelectionMenus] = useState();
const [selectionDishes, setSelectionDishes] = useState();
....
}
Then pass those values to the Menus component:
<Menus
selectionMenus={selectionMenus}
setSelectionMenus={setSelectionMenus}
selectionDishes={selectionDishes}
setSelectionDishes={setSelectionDishes}
nextPage={(menu) => {
setInfo(menu);
setCurrentStep((s) => s + 1);
}}
/>
Subsequently, you will have to remove the state from the Menus component and use the one you receive from props:
export default function Menus({ selectionMenus, setSelectionMenus, selectionDishes, setSelectionDishes }) {
/*const [selectionMenus, setSelectionMenus] = useState({}); //remove this line
const [selectionDishes, setSelectionDishes] = useState({});*/ //remove this line
...
}
Finally, you can use inside your useEffect hook the two states and map them to only get the selected ids:
useEffect(() => {
// ... other logic you had
if(selectionDishes?.length && selectionMenus?.length){
const data = {
date: info.date,
id: selectionDishes.map(d => d.dishId),
idMenus: selectionMenus.map(m => m.menuId)
};
}
}, [info, user, selectionMenus, selectionDishes]);
react-select has options to format the component:
getOptionLabel: option => string => used to format the label or how to present the options in the UI,
getOptionValue: option => any => used to tell the component what's the actual value of each option, here you can return just the id
isOptionSelected: option => boolean => used to know what option is currently selected
onChange: option => void => do whatever you want after the input state has changed
value => any => if you customize the above functions you may want to handle manually the value
Hope it helps you
I changed option but useeffect not update input. Please guide me where i make mistake. First i use useEffect to setCurrency after that i use mapping for getCurrency to add it on Select option. onCitySelect i added it to setSelectedId when i change Select option. Lastly, i tried to get address with api but the problem is i need to change api/address?currency=${selectId.id}` i added selectedId.id but everytime i change option select it is not affect and update with useEffect. I tried different solution couldn't do it. How can i update useEffect eveytime option select change (selectId.id) ?
export default function Golum() {
const router = useRouter();
const dispatch = useDispatch();
const [getCurrency, setCurrency] = useState("");
const [getAddress, setAddress] = useState("");
const [selectCity, setSelectCity] = useState("");
const [selectId, setSelectId] = useState({
id: null,
name: null,
min_deposit_amount: null,
});
const [cityOptions, setCityOptions] = useState([]);
useEffect(() => {
setSelectCity({ label: "Select City", value: null });
setCityOptions({ selectableTokens });
}, []);
const onCitySelect = (e) => {
if (e == null) {
setSelectId({
...selectId,
id: null,
name: null,
min_deposit_amount: null,
});
} else {
setSelectId({
...selectId,
id: e.value.id,
name: e.value.name,
min_deposit_amount: e.value.min_deposit_amount,
});
}
setSelectCity(e);
};
const selectableTokens =
getCurrency &&
getCurrency.map((value, key) => {
return {
value: value,
label: (
<div>
<img
src={`https://central-1.amazonaws.com/assets/icons/icon-${value.id}.png`}
height={20}
className="mr-3"
alt={key}
/>
<span className="mr-3 text-uppercase">{value.id}</span>
<span className="currency-name text-uppercase">
<span>{value.name}</span>
</span>
</div>
),
};
});
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/currencies`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setCurrency(data);
});
}
return () => (mounted = false);
}, []);
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/address?currency=${selectId.id}`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setAddress(data.address);
})
.catch((error) => {});
}
return () => (mounted = false);
}, []);
return (
<div className="row mt-4">
<Select
isClearable
isSearchable
onChange={onCitySelect}
value={selectCity}
options={selectableTokens}
placeholder="Select Coin"
className="col-md-4 selectCurrencyDeposit"
/>
</div>
<div className="row mt-4">
<div className="col-md-4">
<Form>
<Form.Group className="mb-3" controlId="Form.ControlTextarea">
<Form.Control className="addressInput" readOnly defaultValue={getAddress || "No Address"} />
</Form.Group>
</Form>
</div>
</div>
);
}
The second parameter of the useEffect hook is the dependency array. Here you need to specify all the values that can change over time. In case one of the values change, the useEffect hook re-runs.
Since you specified an empty dependency array, the hook only runs on the initial render of the component.
If you want the useEffect hook to re-run in case the selectId.id changes, specify it in the dependency array like this:
useEffect(() => { /* API call */ }, [selectId.id]);
I think you are accessing the e object wrong. e represents the click event and you should access the value with this line
e.target.value.id
e.target.value.value
I am trying to create a dropdown using a dynamic list of items returned from a function call in Typescript but I am having trouble with overwriting the variables defined in the code and constantly getting their initial values as return.
I manage to read the value of dropdown items as returned by the function just fine but am unable to assign those values to the dropdown items parameter. HereΒ΄s my code:
let tempList: myObject[] = []; //unable to overwrite
const myComponent = () => {
let dropdownItems=[{label: ""}]; //unable to overwrite
fetchAllDropdownItems().then((x)=> tempList = x).catch((err)=> console.log(err))
//Converting myObject array to a neutral array
myList.forEach((y)=> dropdownItems.push(y))
console.log(tempList) // returns [{label: "A"}, {label: "B"}]
console.log(dropdownItems)// returns [{label: ""}, {label: "A"}, {label: "B"}]
return (
<GridContainer>
<GridItem>
<Dropdown
items={dropdownItems} // results in initial value of dropdownItems i.e {label: ""}
onChange={(e) => {
console.log(e?.value)
}}
placeholder="Select an option"
/>
</GridItem>
</GridContainer>
);
};
export default myComponent;
And hereΒ΄s how myObject looks like:
export interface myObject {
label: string;
}
Previously I had defined items in my dropdown as items={[{ label: "A" }, {label: "B"}]} which is essentially the same structure as what I am getting from fetchAllDropdownItems-function return but my goal is to not hard code the items.
I need help in figuring out why I am unable to overwrite variables and would appreciate any advice/suggestions. Thanks in advance.
The problem isn't that you aren't updating tempList, it's that your component renders before you do and nothing tells it to re-render because you're not using that list as state.
With the code structure you've shown, you have two options:
Make your entire module wait until you've fetched the list from the server, before even exporting your component, by using top-level await (a stage 3 proposal well on its way to stage 4 ["finished"] with good support in modern bunders)
or
Have your component re-render once the list is received
Here's an example of #1:
const dropdownItems: myObject[] = await fetchAllDropdownItems();
// top-level await βββββββββββββββ^
const myComponent = () => {
return (
<GridContainer>
<GridItem>
<Dropdown
items={dropdownItems}
onChange={(e) => {
console.log(e?.value)
}}
placeholder="Select an option"
/>
</GridItem>
</GridContainer>
);
};
export default myComponent;
Note, again, that it loads the items once then reuses them, loading them when this module is loaded. That means that it may load the items even if your component is never actually used.
There are a bunch of different ways you can spin #2.
Here's one of them:
const dropdownItems: Promise<myObject[]> = fetchAllDropdownItems();
const myComponent = () => {
const [items, setItems] = useState<myObject[] | null>(null);
useEffect(() => {
let unmounted = false;
dropdownItems
.then(items => {
if (!unmounted) {
setItems(items);
}
})
.catch(error => {
// ...handle/display error loading items...
});
return () => {
unmounted = true;
};
}, []);
return (
<GridContainer>
<GridItem>
<Dropdown
items={items || []}
onChange={(e) => {
console.log(e?.value)
}}
placeholder="Select an option"
/>
</GridItem>
</GridContainer>
);
};
export default myComponent;
That loads the items proactively as soon as your module is loaded, and so like #1 it will load them even if your component is never used, but that means the items are (probably) there and ready to be used when your component is first used.
But the "probably" in the last paragraph is just that: The fetch may still be unsettled before your component is used. That's why the component uses the promise. That means that in that example your component will always render twice: Once blank, then again with the list. It'll be fairly quick, but it will be twice. Making it happen just once if the items are already loaded is possible, but markedly complicates the code:
let dropdownItems: myObject[] | null = null;
const dropdownItemsPromise: Promise<myObject[]>
= fetchAllDropdownItems().then(items => {
dropdownItems = items;
return items;
})
.catch(error => {
// ...handle/display error loading items...
});
const myComponent = () => {
const [items, setItems] = useState<myObject[] | null>(dropdownItems);
useEffect(() => {
let unmounted = false;
if (items === null) {
dropdownItemsPromise.then(items => {
if (!unmounted) {
setItems(items);
}
});
}
return () => {
unmounted = true;
};
}, [items]);
return (
<GridContainer>
<GridItem>
<Dropdown
items={items || []}
onChange={(e) => {
console.log(e?.value)
}}
placeholder="Select an option"
/>
</GridItem>
</GridContainer>
);
};
export default myComponent;
Or you might want to wait to load the items until the first time your component is used:
let dropdownItems: myObject[] | null = null;
const myComponent = () => {
const [items, setItems] = useState<myObject[] | null>(dropdownItems);
useEffect(() => {
let unmounted = false;
if (items === null) {
fetchAllDropdownItems().then(items => {
dropdownItems = items;
if (!unmounted) {
setItems(items);
}
})
.catch(error => {
if (!unmounted) {
// ...handle/display error loading items...
}
});
}
return () => {
unmounted = true;
};
}, [items]);
return (
<GridContainer>
<GridItem>
<Dropdown
items={items || []}
onChange={(e) => {
console.log(e?.value)
}}
placeholder="Select an option"
/>
</GridItem>
</GridContainer>
);
};
export default myComponent;
Or you might want to load the items every time the component is used (perhaps they change over time, or the component is rarely used and caching them in the code seems unnecessary):
const myComponent = () => {
const [items, setItems] = useState<myObject[] | null>(dropdownItems);
useEffect(() => {
let unmounted = false;
fetchAllDropdownItems().then(items => {
if (!unmounted) {
setItems(items);
}
})
.catch(error => {
if (!unmounted) {
// ...handle/display error loading items...
}
});
return () => {
unmounted = true;
};
}, [items]);
return (
<GridContainer>
<GridItem>
<Dropdown
items={items || []}
onChange={(e) => {
console.log(e?.value)
}}
placeholder="Select an option"
/>
</GridItem>
</GridContainer>
);
};
export default myComponent;