Can someone help me to understand why this.props doesn't update after i filter it?
Here the slim version of my code
export default class AutoList extends React.Component {
constructor(props) {
super(props);
this.state = {
filterValue: 'all',
isHidden: true,
autoOptValue: ''
}
}
handleOnChangeBrand(evt) {
let selectedValue = evt.target.value;
this.setState({optionValue: selectedValue});
let filtered = this.props.autos.filter((auto) => {
if(auto.brands){
return auto.brands[0] === selectedValue;
}
return false;
});
console.log(this.props.auto) // still same number
console.log(filtered) // less autos. Actual filtered array
}
render() {
let autoDetail = this.props.autos.map(auto => {
return (
<Auto
key={auto.id}
id={auto.id}
name={auto.name}
brands={auto.brands ? auto.brands : false}/>
);
});
return (
<div>
<section>
<select id='autoFilter' className={this.state.isHidden ? 'u-is-hidden' : ''} onChange={this.handleOnChangeBrand.bind(this)} value={this.state.autoOptValue}>
<option value='brand1'> brand1 </option>
<option value='brand2'> brand2 </option>
</select>
</section>
<ul>
{autoDetail}
</ul>
</div>
);
}
So basically i have this.prop.auto is an array of 100 auto, each of them is an object with brand (which is another array) with 2,3 brands each.
I was able to filter, since filtered give me back an array with filtered autos, the correct ones.
But after that, this.props.auto doesn't update, nor does the UI.
I did something similar but sorting the auto by the brands and it works smoothly.
I don't get the difference here
this.props is effectively immutable within a component, so you cannot update the value of this.props.autos. Array#filter is also a pure function, so the array being filtered is not altered, but a new filtered array is returned. This is why when you log filtered in your function you see the filtered array, but this.props.autos is unchanged.
The simple answer to this is to do the filtering within your render method - I have added an initial state for optionValue of false, and within the filter method checked for this and not filtered if it is still false.
export default class AutoList extends React.Component {
constructor(props) {
super(props);
this.state = {
filterValue: 'all',
isHidden: true,
autoOptValue: '',
optionValue: false
}
}
handleOnChangeBrand(evt) {
let selectedValue = evt.target.value;
this.setState({optionValue: selectedValue});
}
render() {
const { optionValue } = this.state;
const autoDetail = this.props.autos
.filter((auto) => {
if (!optionValue) return true;
if(auto.brands){
return auto.brands[0] === optionValue;
}
return false;
})
.map(auto => {
return (
<Auto
key={auto.id}
id={auto.id}
name={auto.name}
brands={auto.brands ? auto.brands : false}/>
);
});
return (
<div>
<section>
<select id='autoFilter' className={this.state.isHidden ? 'u-is-hidden' : ''} onChange={this.handleOnChangeBrand.bind(this)} value={this.state.autoOptValue}>
<option value='brand1'> brand1 </option>
<option value='brand2'> brand2 </option>
</select>
</section>
<ul>
{autoDetail}
</ul>
</div>
);
}
Filter returns a new array. This is so you still have the original on hand. There are so very, very many reasons this is a good thing, and practically 0 reasons it's a bad thing.
Props is meant to be immutable (just like arrays that you call filter on). Don't change the members on props. Nor should you change the members of the members of props, or any descendant data of props in general.
That is why state exists, so if you absolutely must, you can save it there.
Regarding #2, typically you should be pulling that stuff out into higher and higher layers of abstraction, getting away from the view data, completely, and just passing in finished data that's ready for showing.
The Array.prototype.filter() method always returns a new filtered array without changing the old one:
The filter() method creates a new array with all elements that pass the test implemented by the provided function.
In your handleOnChangeBrand event, you're creating a filtered array, without affecting the old one, but then not using that filtered array when React calls render for the second time.
A small example of how you could handle this would be as follows:
1) Have react render your default autos prop
export default class AutoList extends React.Component {
render() {
const autoDetail = this.props.autos.map(auto => {
return (
<Autos ... />
)
});
return (
<ul>
{ autoDetail }
</ul>
);
}
}
2) Add in a click handler and a state value to hold the value what you would like to filter by:
export default class AutoList extends React.Component {
constructor(props) {
super(props);
this.state = {
filter: '' // this is where we will store what we want to filter by
}
};
// All this function will do is update the state which we want to filter by
// this 'arrow function' auto bind 'this' for us, so we don't have to explicitely set .bind as you were doing before
handleOnChangeBrand = (event) => {
this.setState({
filter: event.target.value
})
};
render() {
const autoDetail = this.props.autos.map(auto => {
return (
<Autos ... />
)
});
return (
<ul>
{ autoDetail }
</ul>
);
}
}
3) Finally, we will use the value we are storing in state to filter to the auto brands we would like and use that to build our array
export default class AutoList extends React.Component {
constructor(props) {
super(props);
this.state = {
filter: '' // this is where we will store what we want to filter by
}
};
getFilteredAutos = () => {
// if we have no filter, return everything!
if (this.state.filter === '') {
return this.props.autos;
}
// this is returning the newely filtered array, without affecting the old one
return this.props.autos.filter(auto => {
if(auto.brands){
// we're filtering by our saved value
return auto.brands[0] === this.state.filter;
}
return false;
});
},
handleOnChangeBrand = (event) => {
this.setState({
filter: event.target.value
})
};
render() {
// we're mapping by the filtered results here
const autoDetail = this.getFilteredAutos().map(auto => {
return (
<Autos ... />
)
});
return (
<ul>
{ autoDetail }
</ul>
);
}
}
Related
class Select extends React.PureComponent {
constructor(props) {
super(props)
this.state = { value: this.props.defaultValue }
this.handleChange = this.handleChange.bind(this)
}
handleChange(e) {
e.persist()
if (typeof this.props.onDataChange !== 'undefined') {
this.setState({ value: e.target.value }, () => this.props.onDataChange(e))
} else {
this.setState({ value: e.target.value })
}
}
render() {
const { options } = this.props
return (
<div>
<select
value={this.state.value}
onChange={this.handleChange}
>
{options.map((option, i) => {
const value = option.value || option.path || null
const label = option.label || option.name || option
return (
<option key={`option-${i}`} value={value}>
{label}
</option>
)
})}
</select>
</div>
)
}
}
class Display extends React.PureComponent {
constructor(props) {
super(props)
}
async getSomeValues() {
try {
this.setState({ isReady: false })
await Axios.get(`some-values`)
.then(async (result) => {
this.setState({
values: result.data.values,
default: result.data.default
})
})
} catch (error) {
console.log(error)
} finally {
this.setState({ isReady: true })
}
}
componentDidMount() {
this.getSomeValues()
}
render() {
return (
<Select
options={this.state.values}
defaultValue = {this.state.defaultValue}
/>
)
}
}
I'm trying to solve what i believe to be a pretty simple problem. I have a parent component that houses a child component which is rending a select dropdown.
My parent makes a call to an API service and pulls back a list of items that are to be displayed in the select dropdown. My API returns the set of options to be displayed and an initial value to be selected on load.
The initial render takes the defaultValue property and sets the state to be displayed in the initial instance in the select component constructor, the problem i have with this, is that the api call is done after the initial render so the default value always comes out being null.
I need a mechanism to set the value of the select dropdown to an initial value on load but it has to be done as a result of the api call that happens once the component has loaded?
What is the cleanest way to set the state value to whatever is returned from the API on initial load?
I'm sure this must be an easy problem to solve but i keep getting stuck between what i want to do and the load / render patterns in react.
Any help would be appreciated.
I see two options:
Option 1
You can prevent the rendering of your Select component until the request is finished. This will mean your constructor will fire after you have the data and will be initialized correctly.
render() {
if (this.state.defaultValue) {
return (
<Select
options={this.state.values}
defaultValue={this.state.defaultValue}
/>
)
} else {
return null; // or loading graphic
}
}
Option 2
In your Select component, use a lifecycle method like componentDidUpdate to check if the defaultValue prop has changed from the last render. If so, set the default value in state. This will make it so that defaultValue only gets set once.
componentDidUpdate(prevProps) {
if (this.props.defaultValue !== prevProps.defaultValue) {
this.setState({ defaultValue: this.props.defaultValue });
}
}
In the parent component, I receive data from the server and then map this data into a jsx format. Inside this mapping I have a child component and try to pass a value from state of parent to child as a property, however when I update state of this value, the render function for child is not executed.
Expected behavior: As a user I see a list of items. If I click on an item it should become as checked.
export class ReactSample extends React.Component {
constructor(props){
super(props);
this.state = {
items: [],
mappedItems: [],
selectedIds: [],
isSelected: false,
clickedTripId: null
};
this.toggleSelection = this.toggleSelection.bind(this);
}
componentWillMount(){
console.log("Component mounting")
}
toggleSelection (id, e) {
if(!_.includes(this.state.selectedIds, id)) {
this.setState((state) => ({selectedIds:
state.selectedIds.concat(id)}));
this.setState(() => ({clickedTripId: id}));
this.mapItems(this.state.items);
}
}
componentDidMount() {
const self = this;
MyService.getItems()
.then(res => {
self.setState(() => ({ items: res.allItems }));
self.setState(() => ({ mappedItems:
this.mapItems(res.allItems) }));
}
)
}
mapItems (items) {
return items.map(trip => {
return (
<li key={trip.id} onClick={(e) => (this.toggleSelection(trip.id,
e))}>
<span>{trip.title}</span>
<Tick ticked={this.state.clickedTripId}/>
<span className="close-item"></span>
</li>
);
});
}
getItems() {
}
render() {
return (
<div>
<a className="title">This is a react component!</a>
<Spinner showSpinner={this.state.items.length <= 0}/>
<div className="items-container">
<ul id="itemsList">
{this.state.mappedItems}
</ul>
</div>
</div>
);
}
}
export class Tick extends React.Component {
constructor(props) {
super(props);
}
render() {
console.log('RENDER');
return (<span className={this.props.ticked ? 'tick display' :
'tick hide' }></span>);
}
}
I see a couple issues.
In toggleSelection you aren't doing anything with the result of mapItems. This kind of bug would be much easier to avoid if you just remove mappedItems from state and instead just call mapItems within your render method.
The other issue is you are passing this.state.clickedTripId as the ticked property. I assume you meant to pass something more like this.state.clickedTripId === trip.id.
As Ryan already said, the problem was that mappedItems where not updated when toggleSelection was clicked. As it is obvious from the code mapItems returns data in jsx format. To update it I had to call this.setState({mappedItems: this.mapItems(this.state.items)}) which means that I call mapItems and then I assign the result to the state. In this case my list will be updated and Tick component will receive this.state.clickedItemId as a tick property. There is one more issue that needs to be done to make this code working:
this mapped list needs to be updated after this.state.clickedItemId is updated. The method setState is asynchronous which means that this.setState({mappedItems: this.mapItems(this.state.items)}) has to be called only after this.state.clickedItemId is updated. To achieve this, the setState method can receive a callback function as a second parameter. The code snippet is the following:
toggleSelection (id, e) {
if(!_.includes(this.state.selectedIds, id)) {
this.setState((state) => ({
clickedItemId: id,
selectedIds: state.selectedIds.concat(id)
}), () => this.setState({mappedItems: this.mapItems(this.state.items)}));
}
}
In this case, at the time the mapItems function is executed all data from the state that is needed here will be already updated:
mapItems (items) {
return items.map(item => {
return (
<li key={item.id} onClick={(e) => (this.toggleSelection(item.id, e))}>
<span>{item.title}</span>
<span>{this.state.clickedItemId}</span>
<Tick ticked={this.state.clickedItemId === item.id}/>
<span className="close-item"></span>
</li>
);
});
}
Should intermediate components control parts of state and call props passed to them or should state be lifted higher? I've been going back and forth whether to have the child component utilize local state or have it handled by higher component and pass additional props down.
In this limited example, I have a Main component. I display some data in this component and pass functions to filter the data to a child component. Though, main component doesn't necessarily need to know about when the menuOpen property is changed. However, I need to update menuOpen when handleCancel(), handleSave(), and handleButtonClick() are called.
handleCancel() and handleSave() both modify the data that is displayed so I declare them in the Main component.
Should I be passing all these props through from Main component or use intermediate components to handle smaller portions of local state but also call props from a parent (grandparent etc) component?
Main Component
//Parent component
class Main extends React.Component {
constructor() {
super();
this.state = {
checkBoxes: {
1: {
name: 'Apple',
isChecked: true,
},
//...
},
fruit: {
1: {
name: 'Apple',
},
//...
},
checkedBoxes: [],
};
this.baseState = JSON.stringify(this.state.checkBoxes);
this.fruitFilter = this.fruitFilter.bind(this);
this.handleSave = this.handleSave.bind(this);
this.handleChange = this.handleChange.bind(this);
this.resetState = this.resetState.bind(this);
}
resetState() {
this.setState({checkBoxes: JSON.parse(this.baseState)});
}
//populates the checkedboxs array with name to filter by
handleSave() {
const checkedBoxes = Object.keys(this.state.checkBoxes)
.filter(key => {
//....some logic
});
this.baseState = JSON.stringify(this.state.checkBoxes);
this.setState({checkedBoxes: checkedBoxes});
}
//handles the checkbox toggle
handleChange(e) {
const checkBoxes = {...this.state.checkBoxes};
checkBoxes[e.target.id].isChecked = e.target.checked;
this.setState({checkBoxes: checkBoxes});
}
//filteres the fruit - if nothing is checked return them all
fruitFilter(fruit) {
return Object.keys(fruit)
.filter(key => {
//...filter logic
})
}
render() {
const visibleFruits = this.fruitFilter(this.state.fruit);
return (
<div>
<Filter
resetState={this.resetState}
checkBoxes={this.state.checkBoxes}
handleSave={this.handleSave}
handleChange={this.handleChange}
/>
<div>
<h2>Filtered Fruit</h2>
{Object.keys(visibleFruits).map(key => {
return (
//... renders list of fruit
);
})}
</div>
</div>
);
}
}
Child Component
class Filter extends React.Component {
constructor(props) {
super(props);
this.state = {
menoOpen: false,
};
this.handleCancel = this.handleCancel.bind(this);
this.handleSave = this.handleSave.bind(this);
this.handleButtonClick = this.handleButtonClick.bind(this);
}
handleSave() {
this.setState({menuOpen: false});
this.props.handleSave();
}
handleCancel() {
this.setState({menuOpen: false});
this.props.resetState();
}
handleButtonClick() {
this.setState({menuOpen: !this.state.menuOpen});
}
render() {
return (
<div>
<button onClick={this.handleButtonClick}>Choose Fruits</button>
{this.state.menuOpen && (
<FilterMenu
checkBoxes={this.props.checkBoxes}
handleSave={this.handleSave}
handleCancel={this.handleCancel}
handleChange={this.props.handleChange}
/>
)}
</div>
);
}
}
Grandchild Component
const FilterMenu = ({checkBoxes, handleChange, handleCancel, handleSave}) => {
return (
<div>
{Object.keys(checkBoxes).map(key => {
return (
//... renders dropdown menu
);
})}
<button onClick={handleCancel}>Cancel</button>
<button onClick={handleSave}>Save</button>
</div>
);
};
Refine the separation of concerns and I think you'll like it better.
Define all checkbox event handlers in Filter.
Filter communications with Main via state only.
Don't force Main to evaluate UI components to set state.
Define Main state for Filter to use as needed to avoid the above.
Filter will construct the checkboxes.
Cancel and Save buttons seem like Filter level functions to me.
A FilterMenu component now seems pointless because it does not do anything. Perhaps in the larger architecture it is useful but you can always re-factor it out of Filter when needed
Filter component is the seam in the code that separates action from state.
State is not unnecessarily pushed further down.
Actual functionality is not unnecessarily pushed further up.
Coupling between Main and Filter is reduced. Filter has more reuse potential.
Let's say we got two different React components. One contains reports with dates, the other should show employees that worked that particular month.
So depending on what reports month was clicked, I need to be able to show those employees, but in a second component.
I'm able to get the date that was clicked in the first one but in order to know which employees to show I need to compare that data (from the 1st component) with employees data (second component).
The big question here is - HOW CAN I TRANSFER THAT NEWLY CONSTRUCTED (onClick - Custom function)EVENTS DATA TO THAT SECOND COMPONENT SO I CAN COMPARE THEM ??
You can create a "Parent" component which will render your two components.
The Parent component will have the selected date in the state.
class Parent extends Component {
constructor() {
this.handleDateChange = this.handleDateChange.bind(this);
this.state = { date: null };
}
handleDataChange(date) {
this.setState({ date });
}
render() {
return (
<div>
<Component1 onDateChange={this.handeDataChange} />
<Component2 date={this.state.date} />
</div>
);
}
}
You have to update your Component1 to receive onDateChange, and you have to call that function when the date is updated:
// where the date is updated
this.props.onDateChange(newDate);
Also you have to update your Component2 to receive date (the selected date) which you can use to filter your employees:
// maybe in the render function... you will know the selected date with this.props.date. For example you could do something like this:
const filtered = this.employees.filter(employee => employee.date === this.props.date);
How does this work?
when you select your date in your first component, it will call handleDateChange
... It will update Parent's state
... then Parent's render function will be called (because the state changed)
... then it will pass the new date (stored in the state) to the second component.
Contain the component within a common parent component that's then able to act as a broker for the relevant data.
Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor.
[From: lifting state up]
This is simplified but something like:
class Employees extends Component {
state = {
employees: []
}
async componentDidMount() {
const { clickedDate } = this.props
const employees = await fetchEmployees(clickedDate) // or whatever
this.setState({ employees })
}
render() {
const { employees } = this.state
if (employees.length === 0) {
return
<p>Loading...</p>
}
return (
<div className='employees'>
{
employees.map(employee => (
<p>{employee}</p>
))
}
</div>
)
}
}
const Reports = ({ dates, setClickedDate }) => (
<div className='reports'>
{
dates.map(date => (
<p onClick={() => setClickedDate(date)}>{date}</p>
))
}
</div>
)
class Parent extends Component {
state = {
clickedDate: undefined,
dates: ['dates', 'from', 'somewhere']
}
setClickedDate = clickedDate => this.setState({ clickedDate })
render() {
const { clickedDate } = this.state
return [
<Reports dates={dates} setClickedDate={this.setClickedDate} />,
<Employees clickedDate={clickedDate} />
]
}
}
I have a component a table with many rows (50-100). There is an ability to edit lines and type there text. All lines are stored in redux store. I update store on every keypress and it takes about 1-1.5sec before the typed character is appeared in the textarea. Every line is a separate component.
The approximate structure is bellow (but of course it's much more complex)
#connect(store => {
return {
phrases: store.phrases
};
})
export default class TableContainer extends React.Component {
handlePhraseUpdate = value => {
// update redux store here
};
render() {
return this.props.phrases.map((phrase, index) => {
return (
<Phrase
phrase={phrase}
onPhraseUpdate={this.handlePhraseUpdate}
/>
)
})
}
}
How can I refactor this code?
When dealing with large collections in Redux, it's best to factor you code so that the fewest components re-render when an item in the collection changes.
In your current implementation, when a Phrase is updated, the TableContainer receives a whole new array of phrases and will re-render all of them, even though only one has changed.
By connecting the Phrase component and looking up the individual phrase for each one will result in just the single row updating when the phrase is updated.
It is best to use an id of some kind on the values, for example:
#connect((store, ownProps) => {
return {
phrase: store.phrases[ownProps.id]
}
})
export default class Phrase extends React.Component {
handlePhraseUpdate = value => {
// update redux store here
}
render() {
const { phrase } = this.props
// render phrase
}
}
#connect(store => {
return {
// this could also be stored rather than calculated each time
phrases: store.phrases.map(p => p.id)
};
})
export default class TableContainer extends React.Component {
render() {
return this.props.phrases.map((id) => {
return (
<Phrase key={id} id={id} />
);
});
}
}
If you don't have an id you can use the index in the array:
#connect((store, ownProps) => {
return {
phrase: store.phrases[ownProps.index]
}
})
export default class Phrase extends React.Component {
handlePhraseUpdate = value => {
// update redux store here
}
render() {
const { phrase } = this.props
// render phrase
}
}
#connect(store => {
return {
phraseCount: store.phrases.length
};
})
export default class TableContainer extends React.Component {
render() {
const phrases = []
let index = 0
for (let index = 0; index < this.props.phraseCount; index++) {
phrases.push(<Phrase key={index} index={index} />)
}
return (
<div>
{ phrases }
</div>
)
}
}
Hope this helps.
Most likely your performance issues are not related to redux, but to ReactJS itself.
This is because whenever you update your global object, all your rows are re-rendered. You should re-render only the row that the user is updating.
You can have control over a component render, using the shouldComponentUpdate method.
For example, you want to update the component only if its internal state has changed.