Set values of list of Material UI Autocompletes that fetch options dynamically - reactjs

I have a Material UI Autocomplete combo-box child component class that fetches results as the user types:
...
fetchIngredients(query) {
this.sendAjax('/getOptions', {
data: {
q: query
}
}).then((options) => {
this.setState({
options: options
});
});
}
...
<Autocomplete
options={this.state.options}
value={this.state.value}
onChange={(e, val) => {
this.setState({value: val});
}}
onInputChange={(event, newInputValue) => {
this.fetchIngredients(newInputValue);
}}
renderInput={(params) => {
// Hidden input so that FormData can find the value of this input.
return (<TextField {...params} label="Foo" required/>);
}}
// Required for search as you type implementations:
// https://mui.com/components/autocomplete/#search-as-you-type
filterOptions={(x) => x}
/>
...
This child component is actually rendered as one of many in a list by its parent. Now, say I want the parent component to be able to set the value of each autocomplete programmatically (e.g., to auto-populate a form). How would I go about this?
I understand I could lift the value state up to the parent component and pass it as a prop, but what about the this.state.options? In order to set a default value of the combo-box, I'd actually need to also pass a single set of options such that value is valid. This would mean moving the ajax stuff up to the parent component so that it can pass options as a prop. This is starting to get really messy as now the parent has to manage multiple sets of ajax state for a list of its Autocomplete children.
Any good ideas here? What am I missing? Thanks in advance.

If these are children components making up a form, then I would argue that hoisting the value state up to the parent component makes more sense, even if it does require work refactoring. This makes doing something with the filled-in values much easier and more organized.
Then in your parent component, you have something like this:
constructor(props) {
super(props);
this.state = {
values: [],
options: []
};
}
const fetchIngredients = (query, id) => {
this.sendAjax('/getOptions', {
data: {
q: query
}
}).then((options) => {
this.setState(prevState => {
...prevState,
[id]: options
});
});
}
const setValue = (newValue, id) => {
this.setState(prevState => {
...prevState,
[id]: newValue
};
}
render() {
return (
<>
...
{arrOfInputLabels.map((label, id) => (
<ChildComponent
id={id}
key={id}
value={this.state.values[id]}
options={this.state.options[id]}
fetchIngredients={fetchIngredients}
labelName={label}
/>
)}
...
</>

Related

How does a parent know about the state of a list of child forms using formik?

I have a form consisting of a list of building materials which can be materials already registered for the construction and new materials:
The MaterialBar:
function MaterialBar({
materialBarId,
material,
onDelete,
onChange,
}: MaterialBarProps) {
const { values, handleChange } = useFormik<MaterialBar>({
initialValues: material,
onSubmit: console.log,
enableReinitialize: true,
});
const updateField = (event) => {
handleChange(event);
onChange(materialBarId, values);
};
return (
<DropdownWrapper>
<Dropdown
label="Material"
items={/* A list of available materials */}
selectedItem={values.material}
onSelect={updateField}
/>
.... Other fields
<TextField
name="amount"
id="material-amount"
label="Amount"
type="number"
onChange={updateField}
/>
<DeleteButton onClick={() => onDelete(materialBarId)}>
<img src={remove} />
</DeleteButton>
</DropdownWrapper>
);
}
The parent (ConstructionMaterials):
function ConstructionMaterials({
project,
materials,
attachMaterial,
}: ProjectMaterialsProps) {
const [allMaterials, setAllMaterials] = useState<IProjectMaterial[]>(
getProjectMaterialsFrom(project.materials)
);
const saveAllMaterials = () => {
allMaterials.forEach((newMaterial) => {
if (newMaterial.isNew) {
attachMaterial(newMaterial);
}
});
};
const updateNewMaterial = (
materialBarId: number,
updatedMaterial: MaterialBar
): void => {
const updatedList: IProjectMaterial[] = allMaterials.map((material) => {
if (material.projectMaterialId === materialBarId) {
return {
...material,
materialId: materials.find(
(currentMaterial) =>
currentMaterial.name === updatedMaterial.material
)?.id,
type: updatedMaterial.type,
length: updatedMaterial.length,
width: updatedMaterial.width,
value: updatedMaterial.amount,
};
}
return material;
});
setAllMaterials(updatedList);
};
// Adds a new empty material to the list
const addMaterial = (): void => {
setAllMaterials([
...allMaterials,
{
projectMaterialId: calculateMaterialBarId(),
projectId: project.id,
isNew: true,
},
]);
};
return (
<>
{allMaterials.map((material) => {
const materialBar: MaterialBar = {
material: material.name || "",
type: material.type || "",
amount: material.value || 0,
length: material.length || 0,
width: material.width || 0,
};
return (
<AddMaterialBar
key={material.projectMaterialId}
materialBarId={material.projectMaterialId!}
materials={materials}
onDelete={removeMaterial}
onChange={updateNewMaterial}
/>
);
})}
<Button onClick={() => saveAllMaterials()}>
{texts.BUTTON_SAVE_MATERIALS}
</Button>
</>
);
}
I have a hard time figuring out how to manage the list of materials. I use Formik (the useFormik hook) in the MaterialBar to take care of the values of each field.
My challenge is how to keep all the data clean and easily pass it between the components while knowing which materials are new and which already exist. If I just use Formik in the MaterialBar, then ConstructionMaterials does not know about the changes made in each field and it needs the updated data because it calls the backend with a "save all" action (the "Save"-buttons in the image should not be there, but they are my temporary fix).
To circumvent this, I also keep track of each material in ConstructionMaterials with the onChange on MaterialBar, but that seems redundant, since this is what Formik should take care of. I have also added a isNew field to the material type to keep track of whether it is new, so I don't two lists for existing and new materials.
I have had a look at FieldArray in ConstructionMaterials, but shouldn't Formik be used in the child, since the parent should just grab the data and send it to the API?
So: is there a clever way to handle a list of items where the parent can know about the changes made in the childs form, to make a bulk create request without the parent having to also keep track of all the objects in the children?
Sorry about the long post, but I don't know how to make it shorter without loosing the context.

How to prevent unnecessary re-renders with React Hooks, function components and function depending on item list

List of items to render
Given a list of items (coming from the server):
const itemsFromServer = {
"1": {
id: "1",
value: "test"
},
"2": {
id: "2",
value: "another row"
}
};
Function component for each item
We want to render each item, but only when necessary and something changes:
const Item = React.memo(function Item({ id, value, onChange, onSave }) {
console.log("render", id);
return (
<li>
<input
value={value}
onChange={event => onChange(id, event.target.value)}
/>
<button onClick={() => onSave(id)}>Save</button>
</li>
);
});
ItemList function component with a handleSave function that needs to be memoized.
And there is a possibility to save each individual item:
function ItemList() {
const [items, setItems] = useState(itemsFromServer);
const handleChange = useCallback(
function handleChange(id, value) {
setItems(currentItems => {
return {
...currentItems,
[id]: {
...currentItems[id],
value
}
};
});
},
[setItems]
);
async function handleSave(id) {
const item = items[id];
if (item.value.length < 5) {
alert("Incorrect length.");
return;
}
await save(item);
alert("Save done :)");
}
return (
<ul>
{Object.values(items).map(item => (
<Item
key={item.id}
id={item.id}
value={item.value}
onChange={handleChange}
onSave={handleSave}
/>
))}
</ul>
);
}
How to prevent unnecessary re-renders of each Item when only one item changes?
Currently on each render a new handleSave function is created. When using useCallback the items object is included in the dependency list.
Possible solutions
Pass value as parameter to handleSave, thus removing the items object from the dependency list of handleSave. In this example that would be a decent solution, but for multiple reasons it's not preferred in the real life scenario (eg. lots more parameters etc.).
Use a separate component ItemWrapper where the handleSave function can be memoized.
function ItemWrapper({ item, onChange, onSave }) {
const memoizedOnSave = useCallback(onSave, [item]);
return (
<Item
id={item.id}
value={item.value}
onChange={onChange}
onSave={memoizedOnSave}
/>
);
}
With the useRef() hook, on each change to items write it to the ref and read items from the ref inside the handleSave function.
Keep a variable idToSave in the state. Set this on save. Then trigger the save function with useEffect(() => { /* save */ }, [idToSave]). "Reactively".
Question
All of the solutions above seem not ideal to me. Are there any other ways to prevent creating a new handleSave function on each render for each Item, thus preventing unnecessary re-renders? If not, is there a preferred way to do this?
CodeSandbox: https://codesandbox.io/s/wonderful-tesla-9wcph?file=/src/App.js
The first question I'd like to ask : is it really a problem to re-render ?
You are right that react will re-call every render for every function you have here, but your DOM should not change that much it might not be a big deal.
If you have heavy calculation while rendering Item, then you can memoize the heavy calculations.
If you really want to optimize this code, I see different solutions here:
Simplest solution : change the ItemList to a class component, this way handleSave will be an instance method.
Use an external form library that should work fine: you have powerfull form libraries in final-form, formik or react-hook-form
Another external library : you can try recoiljs that has been build for this specific use-case
Wow this was fun! Hooks are very different then classes. I got it to work by changing your Item component.
const Item = React.memo(
function Item({ id, value, onChange, onSave }) {
console.log("render", id);
return (
<li>
<input
value={value}
onChange={event => onChange(id, event.target.value)}
/>
<button onClick={() => onSave(id)}>Save</button>
</li>
);
},
(prevProps, nextProps) => {
// console.log("PrevProps", prevProps);
// console.log("NextProps", nextProps);
return prevProps.value === nextProps.value;
}
);
By adding the second parameter to React.memo it only updates when the value prop changes. The docs here explain that this is the equivalent of shouldComponentUpdate in classes.
I am not an expert at Hooks so anyone who can confirm or deny my logic, please chime in and let me know but I think that the reason this needs to be done is because the two functions declared in the body of the ItemList component (handleChange and handleSave) are in fact changing on each render. So when the map is happening, it passes in new instances each time for handleChange and handleSave. The Item component detects them as changes and causes a render. By passing the second parameter you can control what the Item component is testing and only check for the value prop being different and ignore the onChange and onSave.
There might be a better Hooks way to do this but I am not sure how. I updated the code sample so you can see it working.
https://codesandbox.io/s/keen-roentgen-5f25f?file=/src/App.js
I've gained some new insights (thanks Dan), and I think I prefer something like this below. Sure it might look a bit complicated for such a simple hello world example, but for real world examples it might be a good fit.
Main changes:
Use a reducer + dispatch for keeping state. Not required, but to make it complete. Then we don't need useCallback for the onChange handler.
Pass down dispatch via context. Not required, but to make it complete. Otherwise just pass down dispatch.
Use an ItemWrapper (or Container) component. Adds an additional component to the tree, but provides value as the structure grows. It also reflects the situation we have: each item has a save functionality that requires the entire item. But the Item component itself does not. ItemWrapper might be seen as something like a save() provider in this scenario ItemWithSave.
To reflect a more real world scenario there is now also a "item is saving" state and the other id that's only used in the save() function.
The final code (also see: https://codesandbox.io/s/autumn-shape-k66wy?file=/src/App.js).
Intial state, items from server
const itemsFromServer = {
"1": {
id: "1",
otherIdForSavingOnly: "1-1",
value: "test",
isSaving: false
},
"2": {
id: "2",
otherIdForSavingOnly: "2-2",
value: "another row",
isSaving: false
}
};
A reducer to manage state
function reducer(currentItems, action) {
switch (action.type) {
case "SET_VALUE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
value: action.value
}
};
case "START_SAVE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
isSaving: true
}
};
case "STOP_SAVE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
isSaving: false
}
};
default:
throw new Error();
}
}
Our ItemList to render all items from the server
export default function ItemList() {
const [items, dispatch] = useReducer(reducer, itemsFromServer);
return (
<ItemListDispatch.Provider value={dispatch}>
<ul>
{Object.values(items).map(item => (
<ItemWrapper key={item.id} item={item} />
))}
</ul>
</ItemListDispatch.Provider>
);
}
The main solution ItemWrapper or ItemWithSave
function ItemWrapper({ item }) {
const dispatch = useContext(ItemListDispatch);
const handleSave = useCallback(
// Could be extracted entirely
async function save() {
if (item.value.length < 5) {
alert("Incorrect length.");
return;
}
dispatch({ type: "START_SAVE", id: item.id });
// Save to API
// eg. this will use otherId that's not necessary for the Item component
await new Promise(resolve => setTimeout(resolve, 1000));
dispatch({ type: "STOP_SAVE", id: item.id });
},
[item, dispatch]
);
return (
<Item
id={item.id}
value={item.value}
isSaving={item.isSaving}
onSave={handleSave}
/>
);
}
Our Item
const Item = React.memo(function Item({ id, value, isSaving, onSave }) {
const dispatch = useContext(ItemListDispatch);
console.log("render", id);
if (isSaving) {
return <li>Saving...</li>;
}
function onChange(event) {
dispatch({ type: "SET_VALUE", id, value: event.target.value });
}
return (
<li>
<input value={value} onChange={onChange} />
<button onClick={onSave}>Save</button>
</li>
);
});

Fetching label value of switch in react native

I want to achieve functionality of a checkbox with a switch in react-native, as checkbox isn't supported in ios devices. I am not getting on how can I fetch the label value associated with a particular switch.
So, the idea is, I have a bunch of options, say A,B,C and each is associated with a switch, and there is a submit button. On click of submit I want to get the labels of those options which are toggled on.
This is the code for selecting various options and each is associated with a switch,
<Text>A</Text>
<Switch
onValueChange = {this.handleToggle}
value = {toggleValue}
/>
<Text>B</Text>
<Switch
onValueChange = {this.handleToggle}
value = {toggleValue}
/>
<Text>C</Text>
<Switch
onValueChange = {this.handleToggle}
value = {toggleValue}
/>
And handleToggle code is this,
handleToggle = event => {
this.setState(state => ({
toggleValue: !state.toggleValue
}));
}
Thanks a lot.
You are using the same function for different switches, clicking on one of them won't give you access to the label of that particular switch. To do so i would suggest to build it like this. This could be a working solution:
Starting with an array that looks like:
this.state = {
data = [
{
label: "firstSwitch",
checked: false,
},
{
label: "secondSwitch",
checked: false,
}
]
}
Then, in your render:
this.state.data.map((item, index) => <Fragment>
<Text>{item.label}</Text>
<Switch
onValueChange = {() => this.handleToggle(index)}
/>
</Fragment>
)
The handleToggle will update the array in the position passed as argument:
handleToggle = index => {
let tempArr= this.state.data
tempArr[index].checked = !tempArr[index].checked
this.setState({data:tempArr})
}
The submit button will then check for the checked switches:
onSubmit = () => {
let arrOfCheckedSwitches= []
this.state.data.forEach (item => item.checked && arrOfCheckedSwitches.push(item.label))
console.log("array of labels :", arrOfCheckedSwitches)
}
Let me know if there's something that's unclear

How to set a state array with values from TextField using onchange

I am new to react and am trying to add string values in an array. I am using Material-UI objects.
My state has
this.state: {
roles: []
}
A button pushes an undefined element in roles, incrementing its length.
clickAddRole = () => {
this.setState({roles: this.state.roles.concat([undefined]) });
};
So now we have some length to the roles array.
The Textfield is generated with
this.state.roles.map((item, i)=> {
return (
<TextField id={'roles['+i+']'} label={'role '+i} key={i} onChange={this.handleChange('roles['+i+']')} />
)
})
the onchange event is handled as below
handleChange = name => event => {
console.log(name);
this.setState({[name]: event.target.value});
console.log(this.state.roles);
}
The console.log statements generate output like
roles[0]
[undefined]
I expect
roles[0]
["somedata"]
what is going wrong here? The data does not get set in the roles array.
The whole code file is
const styles = theme => ({
error: {
verticalAlign: 'middle'
},
textField: {
marginLeft: theme.spacing.unit,
marginRight: theme.spacing.unit,
width: 300
},
submit: {
margin: 'auto',
marginBottom: theme.spacing.unit * 2
}
})
class AddModule extends Component {
constructor() {
super();
this.state = {
roles:[],
open: false,
error: ''
}
}
clickSubmit = () => {
const module = {
roles: this.state.roles || undefined
}
create(module).then((data) => {
if (data.error) {
this.setState({error: data.error})
} else {
this.setState({error: '', 'open': true});
}
})
}
clickAddRole = () => {
this.setState({roles: this.state.roles.concat([undefined]) });
};
handleChange = name => event => {
console.log(name);
this.setState({[name]: event.target.value});
console.log(this.state.roles);
}
render() {
const {classes} = this.props;
return (
<div>
<Button onClick={this.clickAddRole} >Add Role</Button>
{
this.state.roles.map((item, i)=> {
return (
<TextField className={classes.textField} id={'roles['+i+']'} label={'role '+i} key={i} onChange={this.handleChange('roles['+i+']')} />
)
})
}
</div>
)
}
}
I think you're making the whole code a bit overcomplicated creating names for each input field. What I would do is change the handleRolesChange or handleChange (not really sure if you changed its name) method so that it takes the index instead of a name.
handleRolesChange = index => event => {
const { roles } = this.state;
const newRoles = roles.slice(0); // Create a shallow copy of the roles
newRoles[index] = event.target.value; // Set the new value
this.setState({ roles: newRoles });
}
Then change the render method to something like this:
this.state.roles.map((item, index) => (
<TextField
id={`roles[${index}]`}
label={`role ${index}`}
key={index}
onChange={this.handleRolesChange(index)}
/>
))
Guy I have the issue (maybe temporarily).
I an array-element is a child of the array. so changing the data in the array-element does not need setState.
So this is what I did....
handleRolesChange = name => event => {
const i = [name];
this.state.roles[i]=event.target.value;
}
I also change the Textfield onchange parameter to
onChange={this.handleRolesChange(i)}
where i is the index starting from zero in the map function.
All this works perfectly as I needed.
However, if you think that I have mutated the roles array by skipping setState, I will keep the Question unanswered and wait for the correct & legitimate answer.
Thanks a lot for your support guys.
We must try and find the solution for such basic issues. :)
Are you positive it's not being set? From React's docs:
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
Usually logging state in the same block you set the code in will print the previous state, since state has not actually updated at the time the console.log fires.
I would recommend using React Dev Tools to check state, instead of relying on console.log.

React native.- Using setState doesnt change props in textInput

I'm trying to use a text input to filter a ViewList but it seems like because the initial state of 'text' is "", it always loops in a way everytime a type something it goes back to "" deleting everything a type
The filter function goes
filterSearch (texto) {
const newData = this.Data.filter((item) => {
const itemData = item.nombre.toUpperCase()
const textData = this.texto.toUpperCase()
return itemData.indexOf(textData) > -1
})
this.setState({
dataSource: this.state.dataSource.cloneWithRows(newData),
text: texto
})
}
and is called from a textInput
<TextInput
style={styles.busqueda}
placeholder= 'Buscar'
onChangeText={(text) => this.filterSearch.bind(text)}
value={this.state.text}
>
</TextInput>
it seems like filterSearch isnt being called the right way...
There are 2 changes you need to do:
1/ Use the correct syntax to execute your function:
onChangeText={(text) => this.filterSearch(text)}
Actually, the onChangeText function was lexical bound to the component already (by using () => {})
2/ You also need to bind the filterSearch function to the react component too, then you can use this inside it (by either way below):
a) Using lexical binding:
filterSearch = (texto) => {
//...
}
b) Bind the function in your constructor:
constructor(props) {
super(props);
// ...
this.filterSearch = this.filterSearch.bind(this);
}
onChangeText={(text) => this.filterSearch(text)}
or
onChangeText={this.filterSearch.bind(this)}

Resources