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.
Related
Problem
I've parent class which contains list of items and renders component for each item of the list. When some item has been changed (even only one), all items in the list are being rerendered.
So I've tried to implement shouldComponentUpdate(). I am using console.log() to see if it is called but I can't see the log. I've found question shouldComponentUpdate is not never called and tried to return return (JSON.stringify(this.props) !=JSON.stringify(nextProps)); but component still renders itself again. So I've tried just to return false (like do not ever update) but it still does. As the last try I've used PureComponent but it is still being rerendered.
Question
How can I stop children re-rendering if the parent list changes and why is ShouldComponentUpdate never called?
Edit
I've noticed something what I didn't mention in question, I'm sorry for that. I am using context. If I don't use context -> it's ok. Is there any chance to stop re-render while using context? (I'm not using context on updated item - values of context didn't change).
Example
I've parent class which iterates list and renders TaskPreview component for each item of list:
class Dashboard extends React.Component
{
constructor(props) {
this.state = {
tasks: {},
};
}
onTaskUpdate=(task)=>
this.setState(prevState => ({
tasks: {...prevState.tasks, [task._id]: task}
}));
// ... some code
render() {
return (
<div>
{(!Object.entries(this.props.tasks).length)
? null
: this.props.tasks.map((task,index) =>
<TaskPreview key={task._id} task={task} onChange={this.onTaskUpdate}/>
})}
</div>
)
}
}
and I've children TaskPreview class:
class TaskPreview extends React.Component
{
shouldComponentUpdate(nextProps) {
console.log('This log is never shown in console');
return false; // just never!
}
render() {
console.log('task rendered:',this.props.task._id); // indicates rerender
return(<div>Something from props</div>);
}
}
TaskPreview.contextType = TasksContext;
export default TaskPreview;
As #Nicolae Maties suggested I've tried to use Object.keys for iteration instead of direct map but it still doesn't call "shouldComponentUpdate" and still being re-rendered even if there is no changes.
Updated code:
render() {
return (
<div>
{(!Object.entries(this.props.tasks).length)
? null
: Object.keys(this.props.tasks).map((key,index) => {
let task = this.props.tasks[key];
<TaskPreview key={task._id} task={task}/>
}
})}
</div>
)
}
Component is being re-rendered because of .contextType.
TaskPreview.contextType = TasksContext;
Also as is mentioned in documentation:
The propagation from Provider to its descendant consumers (including .contextType and useContext) is not subject to the shouldComponentUpdate method, so the consumer is updated even when an ancestor component skips an update. Source: reactjs.org/docs/context
You have to use context somehow else or do not use it at all.
You can use Context.Consumer which won't force re-render of current component but it might force re-render of its children.
<TasksContext.Consumer>
{value => /* render something based on the context value */}
</TasksContext.Consumer>
Instead of return (JSON.stringify(this.props) != JSON.stringify(nextProps)); in your shouldComponentUpdate() life cycle, try specifying tasks object like this return (JSON.stringify(this.props.tasks) != JSON.stringify(nextProps.tasks));
Maybe react is creating new instances of your component and replaces the old instances with them. That's why you're probably not getting your lifecycle method invoked. That can happen if the key property you're assigning in the map always changes.
use from pureComponent and array as state:
class Dashboard extends React.PureComponent
{
constructor(props) {
this.state = {
tasks: this.props.tasks
}
}
onTaskUpdate=(task)=>
this.setState(prevState => ({
tasks: [...prevState.tasks, task] // render only new task
}));
render() {
const {tasks} = this.state
return (
<div>
{tasks.map(task => <TaskPreview key={task._id} task={task} />)}
</div>
)
}
}
class TaskPreview extends React.PureComponent
{
render() {
console.log('task rendered:',this.props.task._id); // indicates rerender
return(<div>Something from props</div>);
}
}
In the shouldComponentUpdate() method of your TaskPreview component, you should check if the next props have changes in comparison to the current props. Then if there are changes, return true to update the component, otherwise false.
The following example compares all the fields of props object with the new props object. But you can only check the props you are interested in.
shouldComponentUpdate(nextProps) {
return !!(Object.keys(nextProps).find(key => nextProps[key] !== this.props[key]));
}
I tried with below code snippet, shouldComponentUpdate worked as I expected. Could you share your Dashboard initial props ?
class Dashboard extends React.Component {
constructor(props) {
this.state = {
tasks: {}
};
}
onTaskUpdate = task =>
this.setState(prevState => ({
tasks: { ...prevState.tasks, [task._id]: task }
}));
// ... some code
render() {
return (
<div>
{!Object.entries(this.props.tasks).length
? null
: Object.keys(this.props.tasks).map((key, index) => {
let task = this.props.tasks[key];
return (
<TaskPreview
key={task._id}
task={task}
onChange={this.onTaskUpdate}
/>
);
})}
</div>
);
}
}
class TaskPreview extends React.Component {
shouldComponentUpdate(nextProps) {
console.log("This log is never shown in console");
return nextProps.task._id != this.props.task._id;
}
render() {
console.log("task rendered:", this.props.task); // indicates rerender
return (
<button onClick={() => this.props.onChange(this.props.task)}>
Something from props
</button>
);
}
}
my initial props for Dashboard component is :
<Dashboard tasks={{test:{_id:'myId', description:'some description'}}}/>
I'm trying to fetch videos from youtube and display on browser.. I'm able to get videos and update state property. ( checked with console log)
But SearchResults component is not re-rendering when I update the state property. Here is my component
class Application extends React.Component {
state = {videos: []};
handleFormSubmit = async term => {
const res = await youtubeApi.get("search", {
params: {q: term}
});
this.setState({videos: res.data.items})
};
render() {
return (
<div className="container">
<SearchForm onFormSubmit={this.handleFormSubmit}/>
<SearchResults videos={this.state.videos}/>
</div>
)
}
}
SearchResults component
class SearchResults extends React.Component {
state = {featured: null, suggested: []};
componentDidMount() {
if (this.props.videos.length > 0)
this.setState({featured: this.props.videos[0].id.videoId});
if (this.props.videos.length > 1)
this.setState({suggested: this.props.videos.slice(1)});
}
handleSidebarVideoClick = id => {
this.setState({featured: id})
};
render() {
return (
<div className="row">
<div className="col-md-8">
<Video video={this.state.featured}/>
</div>
<div className="col-md-4">
<Sidebar videos={this.state.suggested} onSidebarVideoClick={this.handleSidebarVideoClick}/>
</div>
</div>
)
}
}
You need to have getDerivedStateFromProps() in your SearchResults component in order to expect a new props value when it changes on parent component.
ComponentDidMount is called once the component is mounted, not when it receives prop values when it changes every time.
So you should have something like this:
static getDerivedStateFromProps(nextProps) {
if (nextProps.videos.length > 0) {
return {
featured: nextProps.videos[0].id.videoId
};
}
if (nextProps.videos.length > 1) {
return {
suggested: nextProps.videos.slice(1)
};
}
return null;
}
Ref: https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
It is unlikely you checked the component's state property. You most likely checked this.state which does get updated. The question is what is this.
There is no evidence you bound your event handlers to the component as recommended in the dev guide. So the keyword this doesn't reflect the Component, it reflects the DOM element receiving the event.
To test this, console log the value of this in you event handlers.
To fix:
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.handleSidebarVideoClick = this.handleSidebarVideoClick.bind(this);
in their respective classes constructors.
Ref: https://reactjs.org/docs/handling-events.html
ObjectList has an array of objects that get rendered as a list. When the user clicks a list item, that object is sent back to ObjectEditor so the user can view it and continue editing. The problem is that I'm not sure how to pass that object to ObjectEditor because the click event is taking place in ObjectList.
My initial solution was to pass it to ObjectEditor as props and use the componentWillReceiveProps method to update ObjectEditors state. However, that solution wasn't practical because I don't want it to update every time the props change. Is there a better way?
I'm new to React so I'd like to avoid using Redux for now until I've covered React.
I've heavily cut down the code for clarity.
ObjectList:
constructor(props){
super(props);
this.state = { objects: [
{title: '', items: [], anotherThing:''},
{title: '', items: [], anotherThing:''}
]}
}
viewObject = (index) => {
let object = {...this.state.object[index]};
// Then some code that passes the object to the ObjectEditor Component
}
render(){
return(
<div>
<li key={index} onClick={ () => this.viewObject(index) } >
// A list of titles from state
</li>
<ObjectEditor />
</div>
)
}
ObjectEditor:
constructor(props){
super(props);
this.state = {title:'', items: [], anotherThing:''}
}
// various event handlers that update the state based off form inputs
render(){
return(
<div>
// Various form fields which get pushed to state
<button>Save & Add New</button>
// function that maps through state and renders it to the page
</div>
)
}
}
My suggestion would be to have the parent component handle all the state and logic, and keep the ObjectEditor component a simple presentation component with no logic or state of its own. It would look a little something like this.
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
objects: [
{ title: '', items: [], anotherThing: '' },
{ title: '', items: [], anotherThing: '' }
],
editObject: {},
}
}
viewObject = (index) => {
let object = { ...this.state.object[index] };
this.setState({editObject: object}); // sets the state if the clicked item.
// Then some code that passes the object to the ObjectEditor Component
}
handleChange = (e) => {
// handle change
}
render() {
return (
<div>
<li key={index} onClick={() => this.viewObject(index)} >
// A list of titles from state
</li>
<ObjectEditor viewObject={this.state.viewObject} handleChange={this.handleChange} />
</div>
)
}
}
class ObjectEditor extends React.Component {
render() {
return (
// render some sort of editor
// display data based on the props passed down
// when user edits in the form, call up to the parent's change handler
);
}
}
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} />
]
}
}
In my attempt to handle an update form, have written the code below. It is a controlled input component, with a corresponding state value. When a change happens on the input component the state value is updated. This means view will always reflect data changes and the other way around. My issue comes when trying to prepopulate the input component with data fetched from the database. My attempt was to define the initial state value in the constructor, to be equal to the passed props, but that did not work. When the component is first rendered it will not contain the passed spirit prop, since it has not yet been fetched. When the component is rendered the second time (because the data is ready) the constructor will not be called. How will I set the initial state when the data is ready and not before?
SpiritsEditContainer
export default createContainer(({params}) => {
const handle = Meteor.subscribe("spirit", params.id);
return {
loading: !handle.ready(),
spirit: Spirits.find(params.id).fetch()[0]
}
}, SpiritsEditPage);
SpiritsEditPage
export default class SpiritsEditPage extends Component {
constructor(props) {
super(props)
this.state = {name: this.props.spirit.name}
}
handleNameChange(event) {
this.setState({name: event.target.value});
}
handleUpdate(event) {
event.preventDefault();
}
render() {
const {name} = this.state;
if (this.props.loading) {
return <div>loading</div>
} else {
return (
<div>
<h1>SpiritsEditPage</h1>
<form onSubmit={this.handleUpdate.bind(this)}>
<Input type="text"
label="Name"
value={name}
onChange={this.handleNameChange.bind(this)}/>
<button>Update</button>
</form>
</div>
)
}
}
}
The constructor code may not work correctly:
constructor(props) {
super(props)
this.state = {name: this.props.spirit.name}
}
Instead check for props.spirit to be available.
this.state = { name: this.props.spirit && this.props.spirit.name }
Add a componentWillReceiveProps:
componentWillReceiveProps(nextProps) {
if (nextProps.spirit !== this.props.spirit) {
this.setState({ name: nextProps.spirit.name });
}
}
The rest of the code looks alright.