My goal is to reset the internal state of Autocomplete Material-UI's component.
My custom component is rendered N times in my cycle
{branches.map((branch, index) => {
return (
<BranchSetting
key={index}
index={index}
data={branch}
removeBranch={removeBranch}
/>
)
})}
branch is my hook state.
This is my removeBranch function:
const removeBranch = (index) => {
let listBranch = branches;
listBranch.splice(index, 1);
setBranches([...listBranch]);
}
Every time I delete an item to my array branch, everything works fine except the Autocomplete.
This is my BranchSetting component:
import React, { useState, useEffect } from "react";
import Autocomplete from '#material-ui/lab/Autocomplete';
const BranchSettings = ({ removeBranch, index, branchModify, data }) => {
const [ brands, setBrands ] = useState(data.brands);
const handleBrandSelected = (event, payload) => {
const values = payload.map(item => item.value);
setBrands(values);
}
useEffect(() => {
setBrands(data.brands);
}, [data])
return (
<>
<Autocomplete
id={'branch-brand'}
multiple
disableCloseOnSelect
options={carsBrand}
getOptionLabel={(carsBrand) => carsBrand.label}
onChange={handleBrandSelected}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
variant="outlined"
label={option.value}
size="small"
{...getTagProps({ index })}
/>
))
}
renderInput={(params) => {
return (
<TextField
{...params}
variant="filled"
fullWidth
label={'brand'}
/>
)
}}
/>
</>
)
}
export default BranchSettings
carsBrand it is my data source that in the example I avoided writing the population. It's an array
Everytime I try to delete an item, Autocomplete keep the state of the component ad the prev position.
I'm looking a way to reset all the internal state of Autocomplete component.
The status I refer to can be seen with the devToolBar
I'm looking a good way to keep the items selected properly or that every time the component has changed, rerender the Autocomplete component.
I resolved the problem.
The problem was that Autocomplete component need to input an array of objects with label and value keys.
In the function handleBrandSelected I saved into my brands status just the value. I should have saved the whole object because then it must be sent as input in Autocomplete with the props value.
And to handle the object I should have also used props getOptionSelected.
No problems with the remove function, and no problems with indexes. Only the values selected in inputs and compliant with the documentation were missing.
So this is the new code
import React, { useState, useEffect } from "react";
import Autocomplete from '#material-ui/lab/Autocomplete';
const BranchSettings = ({ removeBranch, index, branchModify, data }) => {
const [ brands, setBrands ] = useState(data.brands);
const handleBrandSelected = (event, payload) => setBrands(payload);
useEffect(() => {
setBrands(data.brands);
}, [data])
return (
<>
<Autocomplete
id={'branch-brand'}
multiple
disableCloseOnSelect
options={carsBrand}
getOptionLabel={(carsBrand) => carsBrand.label}
onChange={handleBrandSelected}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
variant="outlined"
label={option.value}
size="small"
{...getTagProps({ index })}
/>
))
}
renderInput={(params) => {
return (
<TextField
{...params}
variant="filled"
fullWidth
label={'brand'}
/>
)
}}
getOptionSelected={(option, value) => option.value === value.value}
value={brands}
/>
</>
)
}
export default BranchSettings
This problem is probably caused by using the array index as a key in <BranchSetting key={index}. I recommend that you add a unique id when creating the branch object, and use that id as a key instead. You can use performance.now() or a small lib like nanoid.
You can read more about the negative impacts of using an index as a key here.
Related
I would like to delete selected item from list.
When I click on delete the right item get deleted from the list content but on UI I get always the list item fired.
I seems to keep track of JSX keys and show last values.
Here's a demo
const Holidays = (props) => {
console.log(props);
const [state, setState] = useState({ ...props });
useEffect(() => {
setState(props);
console.log(state);
}, []);
const addNewHoliday = () => {
const obj = { start: "12/12", end: "12/13" };
setState(update(state, { daysOffList: { $push: [obj] } }));
};
const deleteHoliday = (i) => {
const objects = state.daysOffList.filter((elm, index) => index != i);
console.log({ objects });
setState(update(state, { daysOffList: { $set: objects } }));
console.log(state.daysOffList);
};
return (
<>
<Header as="h1" content="Select Holidays" />
<Button
primary
icon={<AddIcon />}
text
content="Add new holidays"
onClick={() => addNewHoliday(state)}
/>
{state?.daysOffList?.map((elm, i) => {
console.log(elm.end);
return (
<Flex key={i.toString()} gap="gap.small">
<>
<Header as="h5" content="Start Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.start}/${new Date().getFullYear()}`)
}
/>
</>
<>
<Header as="h5" content="End Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.end}/${new Date().getFullYear()}`)
}
/>
</>
<Button
key={i.toString()}
primary
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(i)}
/>
<span>{JSON.stringify(state.daysOffList)}</span>
</Flex>
);
})}
</>
);
};
export default Holidays;
Update
I'm trying to make a uniq id by adding timeStamp.
return (
<Flex key={`${JSON.stringify(elm)} ${Date.now()}`} gap="gap.small">
<>
<Header as="h5" content="Start Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.start}/${new Date().getFullYear()}`)
}
/>
</>
<>
<Header as="h5" content="End Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.end}/${new Date().getFullYear()}`)
}
/>
</>
<Button
primary
key={`${JSON.stringify(elm)} ${Date.now()}`}
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(i)}
/>{" "}
</Flex>
);
I was hoping that the error disappear but still getting same behaviour
Issue
You are using the array index as the React key and you are mutating the underlying data array. When you click the second entry to delete it, the third element shifts forward to fill the gap and is now assigned the React key for the element just removed. React uses the key to help in reconciliation, if the key remains stable React bails on rerendering the UI.
You also can't console log state immediately after an enqueued state update and expect to see the updated state.
setState(update(state, { daysOffList: { $set: objects } }));
console.log(state.daysOffList);
React state updates are asynchronous and processed between render cycles. The above can, and will, only ever log the state value from the current render cycle, not the update enqueued for the next render cycle.
Solution
Use a GUID for each start/end data object. uuid is a fantastic package for this and has really good uniqueness guarantees and is incredibly simple to use.
import { v4 as uuidV4 } from 'uuid';
// generate unique id
uuidV4();
To specifically address the issues in your code:
Add id properties to your data
const daysOffList = [
{ id: uuidV4(), start: "12/12", end: "12/15" },
{ id: uuidV4(), start: "12/12", end: "12/17" },
{ id: uuidV4(), start: "12/12", end: "12/19" }
];
...
const addNewHoliday = () => {
const obj = {
id: uuidV4(),
start: "12/12",
end: "12/13",
};
setState(update(state, { daysOffList: { $push: [obj] } }));
};
Update handler to consume id to delete
const deleteHoliday = (id) => {
const objects = state.daysOffList.filter((elm) => elm.id !== id);
setState(update(state, { daysOffList: { $set: objects } }));
};
Use the element id property as the React key
{state.daysOffList?.map((elm, i) => {
return (
<Flex key={elm.id} gap="gap.small">
...
</Flex>
);
})}
Pass the element id to the delete handler
<Button
primary
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(elm.id)}
/>
Use an useEffect React hook to log any state update
useEffect(() => {
console.log(state.daysOffList);
}, [state.daysOffList]);
Demo
Note: If you don't want (or can't) install additional 3rd-party dependencies then you can roll your own id generator. This will work in a pinch but you should really go for a real proven solution.
const genId = ((seed = 0) => () => seed++)();
genId(); // 0
genId(); // 1
Hi I'm wondering why this code works,
Sorry for the sintax errors, this is an Example. My question is why memberIpAssignments is taking ip value?. I don't get it if I'm no passing ip into setMemberIpAssignments(arr =>[...arr]) but still takes that's value and updating the state.
setMemberIpAssignments(arr =>[...arr]), this state shouldn't change at all, because I'm no giving ip value. But it does change taking ip value.
if someone can explain to me I'll be grateful.
I'm new at react-native
export const zeroTierNetworkMembersUpdateScreen = ({ route }) => {
const { ipAssignments } = ["192.168.0.1","192.168.1.1"];
const [memberIpAssignments, setMemberIpAssignments] =(ipAssignments);
return (
<View style={styles.viewSet}>
{memberIpAssignments.map((eachIpAssignments, index) => {
return (
<Input
key={index}
placeholder={"ipAssignments"}
keyboardType={"default"}
value={eachIpAssignments}
onChangeText={(value) => {
var ip = ipAssignments;
ip[index] = value;
setMemberIpAssignments(arr =>[...arr]);
}}
/>
);
})}
</View>
);
};
I think I've confirmed my suspicions that you are in fact mutating an object reference that you've stored in local component state.
export const zeroTierNetworkMembersUpdateScreen = ({ route }) => {
// (1) ipAssignments array reference
const ipAssignments = ["192.168.0.1", "192.168.1.1"];
// (2) memberIpAssignments references ipAssignments
const [memberIpAssignments, setMemberIpAssignments] = useState(ipAssignments);
return (
<View style={styles.viewSet}>
{memberIpAssignments.map((eachIpAssignments, index) => {
return (
<Input
key={index}
placeholder={"ipAssignments"}
keyboardType={"default"}
value={eachIpAssignments} // (3) value from memberIpAssignments
onChangeText={(value) => {
// (4) ip references ipAssignments & memberIpAssignments
var ip = ipAssignments;
// (5) element mutation!!
ip[index] = value;
// (6) state update to trigger rerender
setMemberIpAssignments(arr => [...arr]);
}}
/>
);
})}
</View>
);
};
As far as I can tell the mutation happens exactly once since initially everything is reference the original ipAssignments array. Upon updating state though, arr => [...arr] is returning a new array reference for memberIpAssignments the references back to ipAssignments is broken.
You should really be using a functional state update to "edit" the ip entry any way. Consider the following:
export default function App() {
const ipAssignments = ['192.168.0.1', '192.168.1.1'];
const [memberIpAssignments, setMemberIpAssignments] = React.useState(
ipAssignments
);
return (
<View>
{memberIpAssignments.map((eachIpAssignments, index) => {
return (
<TextInput
key={index}
placeholder={'ipAssignments'}
keyboardType={'default'}
value={eachIpAssignments}
onChangeText={(value) => {
setMemberIpAssignments((arr) =>
arr.map((el, i) => (i === index ? value : el))
);
}}
/>
);
})}
</View>
);
}
Expo Snack
I'm using material-ui-chip-input wrapped with Field react-final-form.
I wanted to limit "CHIPS" to 5 only.
Chips are compact elements that represent an input, attribute, or
action.
material ui docs
As you can see I'm using 2 states here
react useStates
react-final-form internal states
This is redundant because react-final-form handled states internally but I can't make to work I'm just showing what I have done so far.
Basically there are
two problems with my implementation.
It doesn't limit my chips.
My react-final-form field values - not updating when clicking DeleteChip
import ChipInput from 'material-ui-chip-input'
import { Field } from 'react-final-form'
const [state, setState] = useState([])
const AddChip = useCallback(
(chip) => {
if (state.length + 1 <= 5) {
setState((prev) => ([...prev.tags, chip]))
}
},
[setState, state.length]
)
const DeleteChip = useCallback(
(chip) => {
setState((prev) => (...prev.state.filter((p) => p !== chip)]
}))
},
[setState]
)
return (
<Field name="states" validate={isRequired} >
{({ input: { value, onChange, ...rest }, meta }) => {
<ChipInput
defaultValue={Array.isArray(value) ? value : []} // check value first because material-ui-chip-input require an array, by default react-final-form value is empty string
onChange={(event) => { // uncontrolled
AddChip(event)
onChange(event)
// I tried below code but same result not working
// if (state.length + 1 <= 5) {
// onChange(event)
// }
}}
onDelete={DeleteChip}
/>
}}
</Field>
)
material-ui-chip-input
react-final-form
see codesandbox demo
This is my take:
https://codesandbox.io/s/proud-water-xp2y1?file=/src/App.js
Key points:
Don't duplicate the state, let react final form handle the state for you
Pass an empty array as the initial state to the FORM, don't pass defaultValues to the Field.
according to the material-ui-chip-input package you need to use onAdd if used in controlled mode, which we do, since we let react final form handle the state.
Add the value prop to the Chipinput.
For cosmetic reasons: don't actually use the render-prop inside <Form /> - use a functional child component instead.
Code:
import ChipInput from "material-ui-chip-input";
import { Form, Field } from "react-final-form";
export default function App() {
return (
<Form
initialValues={{
states: []
}}
onSubmit={() => console.log("submitted")}
>
{({ values, onSubmit }) => (
<form onSubmit={onSubmit}>
<Field name="states">
{({ input: { value, onChange } }) => (
<ChipInput
value={value}
alwaysShowPlaceholder={true}
placeholder="type states here"
onAdd={(newVal) => {
if (value.length >= 5) return;
const newArr = [...value, newVal];
onChange(newArr);
}}
onDelete={(deletedVal) => {
const newArr = value.filter((state) => state !== deletedVal);
onChange(newArr);
}}
/>
)}
</Field>
<p>react useStates</p>
<p>react-final-form values</p>
<pre
style={{
backgroundColor: "rgba(0,0,0,0.1)",
padding: "20px"
}}
>
{JSON.stringify(values, 0, 2)}
</pre>
</form>
)}
</Form>
);
}
I am working with react and the component autocomplete of material-ui, I need help with the following problem.
In the examples of autocomplete I saw you need to have all elements of the list in the frontend to use the autocomplete, in my case I get the list from a web service and it could be huge, so instead of searching for the whole list I want that every time a letter is typed in the autocomplete it generates a search to the web service filtering names according to the input that is being written and with a max results of 10 elements. The endpoint of the webservice already has a filter property where you can pass the quantity of results you want and the letters you want of the name.The only thing that the autocomplete has to do is everytime you type a letter it hits the endpoint (filtering with the word that is being typed) and updates the list of elements of the autocomplete.
Right now I have the following code, the problem is that it searches the whole list when you click the autocomplete but when you type each letter it doesn't do anything.
import Autocomplete from '#material-ui/lab/Autocomplete';
import TextField from '#material-ui/core/TextField';
import CircularProgress from '#material-ui/core/CircularProgress';
const [open, setOpen] = React.useState(false);
const [organizationList, setOrganizationList] = React.useState([]);
const loading = open && organizationList.length === 0;
React.useEffect(() => {
let active = true;
if (!loading) {
return undefined;
}
(async () => {
if (active) {
try {
setOrganizationList(await api.post('/v1/organizations/search', {maxResults:10}));
} catch (error) {
snackbar.showMessage(error, "error");
}
}
})();
return () => {
active = false;
};
}, [loading]);
React.useEffect(() => {
if (!open) {
setOrganizationList([]);
}
}, [open]);
The definition of the autocomplete:
<Autocomplete
id="asynchronous-demo"
style={{ width: 300 }}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionSelected={(option, value) => option.orgName === value.orgName}
getOptionLabel={(option) => option.orgName}
options={organizationList}
loading={loading}
renderInput={(params) => (
<TextField
{...params}
label="Asynchronous"
variant="outlined"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
To hit the endpoint I have this:
setOrganizationList(await api.post('/v1/organizations/search', {maxResults:10}));
I need to pass the input of the autocomplete every time a letter is typed, like this:
setOrganizationList(await api.post('/v1/organizations/search', {name:inputAutocomplete,maxResults:10}));
Thanks a lot for the help.
Im new to react by the way.
In material-ui library Autocomplete component has a props onChange that can be used like this.
onChange={(event, newValue) => {
setValue(newValue);
}}
You should be interested in the second parameter newValue. Thus, you will receive a new input value every time a letter is typed.
Therefore, just move the logic for getting the list into this callback.
You can read more about controllable state in the material-ui documentation
Implementing the onChange function and giving it the function you already made should give you the solution you want.
I converted a class component into a function component using hooks. Currently, I'm struggling to figure out why the checkboxes within this map is not updating with checked value, despite the onChange handler firing, and updating the array as necessary. (The onSubmit also works, and updates the value within the DB properly).
import {
Container,
Typography,
Grid,
Checkbox,
FormControlLabel,
Button
} from "#material-ui/core";
import Select from "react-select";
import localeSelect from "../services/localeSelect";
import {
linkCharactersToGame,
characterLinked,
linkCharacters
} from "../data/locales";
import dbLocale from "../services/dbLocale";
import { LanguageContext } from "../contexts/LanguageContext";
import { UserContext } from "../contexts/UserContext";
import { GameContext } from "../contexts/GameContext";
import { CharacterContext } from "../contexts/CharacterContext";
import { Redirect } from "react-router-dom";
export default function LinkCharacter() {
const { language } = useContext(LanguageContext);
const { user } = useContext(UserContext);
const { games, loading, error, success, connectCharacters } = useContext(
GameContext
);
const { characters } = useContext(CharacterContext);
const [game, setGame] = useState("");
const [selectedCharacters, setSelectedCharacters] = useState([]);
if (!user) {
return <Redirect to="/" />;
}
return (
<section className="link-character">
<Container maxWidth="sm">
<Typography variant="h5">
{localeSelect(language, linkCharactersToGame)}
</Typography>
{error && (
<p className="error">
<span>Error:</span> {error}
</p>
)}
{success && <p>{localeSelect(language, characterLinked)}</p>}
<Select
options={games.map(game => {
return {
label: dbLocale(language, game),
value: game._id
};
})}
onChange={e => {
setGame(e.value);
const selected = [];
const index = games.findIndex(x => x._id === e.value);
games[index].characters.forEach(character => {
selected.push(character._id);
});
setSelectedCharacters(selected);
}}
/>
</Container>
<Container maxWidth="md">
{game !== "" && (
<>
<Grid container spacing={2}>
{characters.map((character, index) => {
return (
<Grid item key={index} md={3} sm={4} xs={6}>
<FormControlLabel
control={
<Checkbox
value={character._id}
onChange={e => {
const index = selectedCharacters.indexOf(
e.target.value
);
if (index === -1) {
selectedCharacters.push(e.target.value);
} else {
selectedCharacters.splice(index, 1);
}
}}
color="primary"
checked={
selectedCharacters.indexOf(character._id) !== -1
}
/>
}
label={dbLocale(language, character)}
/>
</Grid>
);
})}
</Grid>
<Button
variant="contained"
color="primary"
onClick={e => {
e.preventDefault();
connectCharacters(game, selectedCharacters);
}}
>
{localeSelect(language, linkCharacters)}
</Button>
</>
)}
</Container>
</section>
);
}
I feel like there's something I'm missing within Hooks (or there's some sort of issue with Hooks handling something like this). I have been searching and asking around and no one else has been able to figure out this issue as well.
The state returned by [state, setState] = useState([]) is something that you should only be reading from. If you modify it, React won't know that the data has changed and that it needs to re-render. When you need to modify data, you have to use setState, or in your case setSelectedCharacters.
Also, modifying the data by reference might lead to unpredictable results if the array is read elsewhere, later on.
In addition to that, if you give the same value to setState, that the hook returned you in state, React will skip the update entirely. It is not a problem when using numbers or strings, but it becomes one when you use arrays, because the reference (the value React uses to tell if there is a difference) can be the same, when the content might have changed. So you must pass a new array to setState.
With that in mind, your onChange function could look like:
onChange={e => {
const index = selectedCharacters.indexOf(
e.target.value
);
if (index === -1) {
// creating a new array with [], so the original one stays intact
setSelectedCharacters([...selectedCharacters, e.target.value]);
} else {
// Array.filter also creates new array
setSelectedCharacters(selectedCharacters.filter((char, i) => i !== index));
}
}}
Doc is here https://en.reactjs.org/docs/hooks-reference.html#usestate