Component not updating on Map (dictionary) state change using hooks - javascript

I have a child component that is supposed to display names based on a visibility filter with checkboxes. I use a dictionary to keep track of the checked state for each name. However, when I update the dictionary the child does not update.
Here is an example: https://codesandbox.io/s/8k39xmxl52
These are the components:
const App = () => {
const [names, setNames] = useState(seedNames);
const [hidden, setHidden] = useState(new Map());
const handleHidden = e => {
const name = e.target.name;
const hidden = e.target.checked;
setHidden(hidden.set(name, hidden));
};
return (
<div className="App">
<VisibilityFilter
names={names}
hidden={hidden}
handleHidden={handleHidden}
/>
<DisplayNames names={names} hidden={hidden} />
</div>
);
};
const VisibilityFilter = ({ names, hidden, handleHidden }) => {
return (
<div>
{names.map(name => (
<div key={name}>
<input
type="checkbox"
name={name}
checked={hidden.get(name)}
onChange={handleHidden}
defaultChecked
/>
<span>{name}</span>
</div>
))}
</div>
);
};
const DisplayNames = ({ names, hidden }) => {
const visibleNames = names.filter(name => !hidden.get(name));
return (
<div>
{visibleNames.map(name => (
<div key={name}>{name}</div>
))}
</div>
);
};

The use of immutable state is idiomatic to React, it's expected that simple types like plain objects and arrays are used.
Map set returns the same map, while useState supports only immutable states. In case setter function is called with the same state, a component is not updated:
setHidden(hidden.set(name, hidden))
The state should be kept immutable:
setHidden(new Map(hidden.set(name, hidden)))
This may introduce some overhead and defy possible benefits of ES6 Map. Unless Map features like strict item order and non-string keys are in demand, it's preferable to use plain object for the same purpose.

Related

Trying to filter an array using checkboxes and useState, checkbox doesn't stay checked

i have an array, called reportsData, then i need to filter it, generating some checkboxes with each of them having a label based on each name that comes from another array (emittersData), so basically i set it like this:
const [searchUser, setSearchUser] = useState<string[]>([])
const mappedAndFiltered = reportsData
.filter((value: any) =>
searchUser.length > 0 ? searchUser.includes(value.user.name) : true
)
Then i render my checkboxes like this:
function EmittersCheckboxes () {
const [checkedState, setCheckedState] = useState(
new Array(emittersData.length).fill(false)
)
const handleOnChange = (position: any, label: any) => {
const updatedCheckedState = checkedState.map((item, index) =>
index === position ? !item : item
)
setSearchUser((prev) =>
prev.some((item) => item === label)
? prev.filter((item) => item !== label)
: [...prev, label]
)
setCheckedState(updatedCheckedState)
};
return (
<div className="App">
{emittersData.map((value: any, index: any) => {
return (
<li key={index}>
<div className="toppings-list-item">
<div className="left-section">
<input
className="h-4 w-4 focus:bg-indigo border-2 border-gray-300 rounded"
type="checkbox"
id={`custom-checkbox-${index}`}
name={value.Attributes[2].Value}
value={value.Attributes[2].Value}
checked={checkedState[index]}
onChange={() => handleOnChange(index, value.Attributes[2].Value)}
/>
<label className="ml-3 font-medium text-sm text-gray-700 dark:text-primary" htmlFor={`custom-checkbox-${index}`}>{value.Attributes[2].Value}</label>
</div>
</div>
</li>
);
})}
</div>
)
}
And on the react component i am rendering each checkbox, that is a li, like:
<ul><EmittersCheckboxes /></ul>
And i render the mappedAndFiltered on the end.
Then it is fine, when i click each generated checkbox, it filters the array setting the state in setSearch user and the array is filtered.
You can check it here: streamable. com /v6bpk6
See that the filter is working, the total number of items in the array is changing based on the checkbox selected (one or more).
But the thing is that each checkbox does not become 'checked', it remains blank (untoggled).
What am i doing wrong, why doesnt it check itself?
You've defined your EmittersCheckboxes component inside another component. and every time that the parent component renders (by state change) your internal component is redefined, again and again causing it to lose it's internal state that React holds for you.
Here's a simplified example:
import React, { useState } from "react";
function CheckboxeComponent() {
const [checkedState, setCheckedState] = useState(false);
return (
<div>
<span>CheckboxeComponent</span>
<input
type="checkbox"
checked={checkedState}
onChange={() => setCheckedState((x) => !x)}
/>
</div>
);
}
export default function App() {
const [counter, setCounter] = useState(1);
function InternalCheckboxeComponent() {
const [checkedState, setCheckedState] = useState(false);
return (
<div>
<span>InternalCheckboxeComponent</span>
<input
type="checkbox"
checked={checkedState}
onChange={() => setCheckedState((x) => !x)}
/>
</div>
);
}
return (
<>
<InternalCheckboxeComponent />
<CheckboxeComponent />
<button onClick={() => setCounter((c) => c + 1)}>{counter}</button>
</>
);
}
There's the App (parent component) with its own state (counter), with a button to change this state, clicking this button will increase the counter, causing a re-render of App. This re-render redefines a new Component named InternalCheckboxeComponent every render.
The InternalCheckboxeComponent also has an internal state (checkedState).
And there's an externally defined functional component named CheckboxeComponent, with this component React is able to hold its own state, because it's not redefined (It's the same function)
If you set the state of each to be "checked" and click the button, this will cause a re-render of App, this will redefine the InternalCheckboxeComponent function, causing React to lose its state. and the CheckboxeComponent state remains in React as it's the same function.

React - How to prevent re-rendering of all the input fields when input changes

I am implementing a form which is generated using a Json. The Json is retrieved from API and then looping over the items I render the input elements. Here is the sample Json :
{
name: {
elementType: 'input',
label: 'Name',
elementConfig: {
type: 'text',
placeholder: 'Enter name'
},
value: '',
validation: {
required: true
},
valid: false,
touched: false
}
}
Here is how I render the form :
render() {
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
const itemPerRow = 4;
const rows = [
...Array(Math.ceil(props.formElementsArray.length / itemPerRow))
];
const formElementRows = rows.map((row, idx) =>
props.formElementsArray.slice(
idx * itemPerRow,
idx * itemPerRow + itemPerRow
)
);
const content = formElementRows.map((row, idx) => (
<div className='row' key={idx}>
{row.map((formElement) => (
<div className='col-md-3' key={formElement.id}>
<Input
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
handleChange={(event) => props.changed(event, formElement.id)}
/>
</div>
))}
</div>
...
}
I am storing the form state in redux and on every input change , I update the state. Now the problem is everytime I update the state, the entire form is re-rendered again... Is there any way to optimise it in such a way that only the form element which got updated is re-rendered ?
Edit :
I have used React.memo in Input.js as :
export default React.memo(input);
My stateful Component is Pure component.
The Parent is class component.
Edit 2 :
Here is how I create formElementArray :
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
You can make content as a separate component like this.
And remove formElementsArray prop from parent component.
export default function Content() {
const formElementRows = useForElementRows();
formElementRows.map((row, idx) => (
<Input
formId={formElement.id}
handleChange={props.changed}
/>
)
}
Inside Input.js
const handleInputChange = useCallback((event) => {
handleChange(event, formId);
}, [formId, handleChange]);
<input handleChange={handleInputChange} />
export default React.memo(Input)
So you can memoize handleChange effectively. And it will allow us to prevent other <Input /> 's unnecessary renders.
By doing this forElementRows change will not cause any rerender for other components.
You could try a container, as TianYu stated; you are passing a new reference as change handler and that causes not only the component to re create jsx but also causes virtual DOM compare to fail and React will re render all inputs.
You can create a container for Input that is a pure component:
const InputContainer = React.memo(function InputContainer({
id,
elementType,
elementConfig,
value,
invalid,
shouldValidate,
touched,
label,
changed,
}) {
//create handler only on mount or when changed or id changes
const handleChange = React.useCallback(
(event) => changed(event, id),
[changed, id]
);
return (
<Input
elementType={elementType}
elementConfig={elementConfig}
value={value}
invalid={invalid}
shouldValidate={shouldValidate}
touched={touched}
label={label}
handleChange={handleChange}
/>
);
});
Render your InputContainer components:
{row.map((formElement) => (
<div className="col-md-3" key={formElement.id}>
<InputContainer
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
//re rendering depends on the parent if it re creates
// changed or not
changed={props.changed}
/>
</div>
))}
You have to follow some steps to stop re-rendering. To do that we have to use useMemo() hook.
First Inside Input.jsx memoize this component like the following.
export default React.memo(Input);
Then inside Content.jsx, memoize the value of elementConfig, shouldValidate, handleChange props. Because values of these props are object type (non-primitive/reference type). That's why every time you are passing these props, they are not equal to the value previously passed to that prop even their value is the same (memory location different).
const elementConfig = useMemo(() => formElement.config.elementConfig, [formElement]);
const shouldValidate = useMemo(() => formElement.config.validation, [formElement]);
const handleChange = useCallback((event) => props.changed(event, formElement.id), [formElement]);
return <..>
<Input
elementConfig={elementConfig }
shouldValidate={elementConfig}
handleChange={handleChange}
/>
<../>
As per my knowledge, this should work. Let me know whether it helps or not. Thanks, brother.

export Hooks in React for Nested Components?

I'm exporting hooks with nested components so that the parent can toggle state of a child. How can I make this toggle work with hooks instead of classic classes or old school functions?
Child Component
export let visible;
export let setVisible = () => {};
export const ToggleSwitch = () => {
const [visible, setVisibile] = useState(false);
return visible && (
<MyComponent />
)
}
Parent
import * as ToggleSwitch from "ToggleSwitch";
export const Parent: React.FC<props> = (props) => {
return (
<div>
<button onClick={() => ToggleSwitch.setVisible(true)} />
</div>
)
}
Error: Linter says [setVisible] is unused variable in the child... (but required in the parent)
You can move visible state to parent like this:
const Child = ({ visible }) => {
return visible && <h2>Child</h2>;
};
const Parent = () => {
const [visible, setVisible] = React.useState(false);
return (
<div>
<h1>Parent</h1>
<Child visible={visible} />
<button onClick={() => setVisible(visible => !visible)}>
Toggle
</button>
</div>
);
};
If you have many child-components you should make more complex logic in setVisible. Put object to useState where properties of that object will be all names(Ids) of child-components
as you know React is one-way data binding so if you wanna pass any props or state you have only one way to do that by passing it from parent to child component and if the logic becomes bigger you have to make it as a global state by using state management library or context API with react hooks use reducer and use effect.

Getting a value of inputs populated Dynamically React.js

I am pretty new to React, I have worked on react native before, so I am quite familiar with a framework. Basically I have an array of objects, lets say in contains 5 items. I populated views based on the amount of objects, so if there are 5 objects, my map function would populate 5 together with 5 inputs. My question is how can I get a value of each input?
Here is my code:
array.map(map((item, index) => (
<h1> item.title </h1>
<input value={input from user} />
)
You have to use the state and update the value with onChange manually
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
value: ''
}
}
handleInputChange(e) {
this.setState({
[e.target.name]: e.target.value
});
}
render () {
return (
<div>
<input value={this.state.value} onChange={(e) => {this.handleInputChange(e)}} />
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('app'))
A quick solution would be to use an array for all the input values.
const Inputs = ({array}) => {
const [inputs, setInputs] = useState([]);
const setInputAtIndex = (value, index) => {
const nextInputs = [...inputs]; // this can be expensive
nextInputs.splice(index, 1, value);
setInputs(nextInputs);
}
return (
...
array.map((item, index) => (
<div key={index}>
<h1>{item.title}</h1>
<input
value={inputs[index]}
onChange={({target: {value}) => setInputAtIndex(value, index)}
/>
</div>
)
...
);
}
Keep in mind here that in this case every time an input is changed, the inputs state array is copied with [...inputs]. This is a performance issue if your array contains a lot of items.

binding input to variable inside a React Dumb Component with MobX

While learning to use MobX I wanted to update a string from an <input/>.
I know that in Smart Components I can just use onChange={this.variable.bind(this)}, but I don't understand how I can do so in the following scenario:
const dumbComponent = observer(({ prop }) => {
// prop is an object
// destruct1 is a string, destruct2 is an array
const { destruct1, destruct2 } = prop;
const list = destruct2.map((item, key) => (<li key={key} >{item}</li>));
return (
<div>
<h1>title</h1>
<h2>{destruct1}</h2>
// Relevent part start
<input classname="destruct" value={destruct1.bind(this)} />
// Relevent part end
<ul>{list}</ul>
</div>
);
});
export default TodoList;
Can I bind the value of input to destruct somehow?
Obviously, this code doesn't work. But I don't know what to do.
You could create an inline arrow function and alter the prop.destruct1 like this:
const dumbComponent = observer(({ prop }) => {
const { destruct1, destruct2 } = prop;
const list = destruct2.map((item, key) => <li key={key}>{item}</li>);
return (
<div>
<h1>title</h1>
<h2>{destruct1}</h2>
<input
classname="destruct"
value={destruct1}
onChange={e => prop.destruct1 = e.target.value}
/>
<ul>{list}</ul>
</div>
);
});

Categories