Re-render component based on object updating - reactjs

I have the following pattern
class List {
list: string[] = [];
showList() {
return this.list.map(element => <div>{element}</div>);
}
showOptions() {
return (
<div>
<div onClick={() => this.addToList('value1')}>Value #1</div>
<div onClick={() => this.addToList('value2')}>Value #2</div>
<div onClick={() => this.addToList('value3')}>Value #3</div>
<div onClick={() => this.addToList('value4')}>Value #4</div>
</div>
);
}
addToList(value: string) {
this.list.push(value);
}
}
class App extends Component {
myList: List;
constructor(props: any) {
super(props);
this.myList = new List();
}
render() {
<div>
Hey this is my app
{this.myList.showOptions()}
<div>{this.myList.showList()}</div>
</div>
}
}
It shows my options fine, and elements are added to the list when I click on it. However, the showList function is never called again from App, thus not showing any update.
How can I tell the main component to rerenders when List is updated ? I'm not sure my design pattern is good. My goal is to manage what my class displays inside itself, and just call the display functions from other components.

We should always use state to rerender react component.

Not sure what you want to accomplish exactly but hopefully this will give you a general idea what Jim means with using state:
const Option = React.memo(function Option({
value,
onClick,
}) {
return <div onClick={() => onClick(value)}>{value}</div>;
});
const Options = React.memo(function Options({
options,
onClick,
}) {
return (
<div>
{options.map(value => (
<Option
key={value}
value={value}
onClick={onClick}
/>
))}
</div>
);
});
class List extends React.PureComponent {
state = {
options: [1, 2, 3],
selected: [],
};
showList() {
return this.list.map(element => <div>{element}</div>);
}
add = (
value //arrow funcion to bind this
) =>
this.setState({
options: this.state.options.filter(o => o !== value),
selected: [...this.state.selected, value],
});
remove = (
value //arrow funcion to bind this
) =>
this.setState({
selected: this.state.selected.filter(
o => o !== value
),
options: [...this.state.options, value],
});
render() {
return (
<div>
<div>
<h4>options</h4>
<Options
options={this.state.options}
onClick={this.add}
/>
</div>
<div>
<h4>choosen options</h4>
<Options
options={this.state.selected}
onClick={this.remove}
/>
</div>
</div>
);
}
}
const App = () => <List />;
//render app
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Related

React .map and setState weird behavior

The following is a simplified version of the part of the entire code.
The entire app is basically supposed to be a note taking app built on React, and currently I'm stuck on its respective note's editing function.
So the following script part basically is supported to do:
Render an array of <Card /> components based on the App this.state.notes array
By clicking the Edit note button it sets the App this.state.noteEditingId state
(so the React instance can know later which generated Card is currnetly being edited by the id)
By clicking the Save Edit button it tries to update the App this.state.notes array with the submitted edit text.
(see, I used a lot of filters to try to achieve this, since I don't have a good idea to how to more nicely achieve this. I believe there should be a nicer way)
But the result is not what I expect.
(While it supposed to achieve updating the expected Card component's notes array with the new note instance's new note "note" text,
it updates the notes array with the different notes's note instance's "note" text. I cannot explain this clearly, since this is an idk-what-is-wrong type of issue to me. )
const Card = (props) => {
const [noteEditing, setNoteEditing] = useState(false);
return (
<div {...props}>
<div>
<div>
<span>
<button onClick={() => {
noteEditing ? setNoteEditing(false) : setNoteEditing(true);
props.thisApp.setState({ noteEditingId: props.config.id })
}}>Edit note</button>
</span>
{noteEditing
?
<div>
<textarea className='__text' />
<button onClick={() => {
let note = document.querySelector('.__text').value
let current_note = props.thisApp.state.notes.filter(a => a.id == props.config.id)[0]
let notesAfterRemoved = props.thisApp.state.notes.filter(a => a.id !== props.config.id)
if (props.thisApp.state.noteEditingId == props.config.id)
{
props.thisApp.setState({
notes: [...notesAfterRemoved, { ...current_note, note: note }]
})
}
}}>
Save Edit
</button>
</div>
: ""
}
</div>
</div>
</div>
)
}
class App extends React.Component {
constructor() {
super();
this.state = {
notes: [
{
note: "note1.",
id: nanoid(),
},
{
note: "note2.",
id: nanoid(),
},
{
note: "note3.",
id: nanoid(),
},
]
};
}
render() {
return (
<div>
<h2>
Notes ({this.state.notes.length})
</h2>
<div className='__main_cards'>
<div>
{this.state.notes.map((a, i) => {
return <Card key={i} className="__card" thisApp={this} config={
{
note: a.note,
}
} />
})}
</div>
</div>
</div>
)
}
}
So what can I do fix to make the part work properly? Thanks.
You should pass the current note also and get rid of the filter in card component:
this.state.notes.map((note, i) => {
return (
<Card
key={i}
className="__card"
thisApp={this}
currentNote={note}
/>
);
})
And then remove this:
let current_note = props.thisApp.state.notes.filter(a => a.id == props.config.id)[0]
And then rather than finding the notes without the edited note you can create a new array with edited data like this:
const x = props.thisApp.state.notes.map((n) => {
if (n.id === props.currentNote.id) {
return {...n, note}
}
return n
})
And get rid off this line:
let notesAfterRemoved = props.thisApp.state.notes.filter(a => a.id !== props.config.id)
Also, change this
noteEditing ? setNoteEditing(false) : setNoteEditing(true);
To:
setNoteEditing(prev => !prev)
As it is much cleaner way to toggle the value.
And I believe, there is no need to check that if the noteEditingId is equal to current active note id.
So you can get rid off that also (Correct me if I am wrong!)
Here's the full code:
const Card = (props) => {
const [noteEditing, setNoteEditing] = useState(false);
const textareaRef = useRef();
return (
<div {...props}>
<div>
<div>
<span>
<button
onClick={() => {
setNoteEditing((prev) => !prev); // Cleaner way to toggle
}}
>
Edit note
</button>
</span>
{noteEditing && (
<div>
<textarea className="__text" ref={textareaRef} />
<button
onClick={() => {
let note = textareaRef.current.value;
const x = props.thisApp.state.notes.map(
(n) => {
if (n.id === props.currentNote.id) {
return { ...n, note };
}
return n;
}
);
props.thisApp.setState({
notes: x,
});
}}
>
Save Edit
</button>
</div>
)}
</div>
</div>
</div>
);
};
class App extends React.Component {
constructor() {
super();
this.state = {
notes: [
{
note: "note1.",
id: nanoid(),
},
{
note: "note2.",
id: nanoid(),
},
{
note: "note3.",
id: nanoid(),
},
],
};
}
render() {
return (
<div>
<h2>Notes ({this.state.notes.length})</h2>
<div className="__main_cards">
<div>
{this.state.notes.map((note, i) => {
return (
<Card
key={i}
className="__card"
thisApp={this}
currentNote={note}
/>
);
})}
</div>
</div>
</div>
);
}
}
}
);
props.thisApp.setState({
notes: x,
});
}
}}
>
Save Edit
</button>
</div>
)}
</div>
</div>
</div>
);
};
class App extends React.Component {
constructor() {
super();
this.state = {
notes: [
{
note: "note1.",
id: nanoid(),
},
{
note: "note2.",
id: nanoid(),
},
{
note: "note3.",
id: nanoid(),
},
],
};
}
render() {
return (
<div>
<h2>Notes ({this.state.notes.length})</h2>
<div className="__main_cards">
<div>
{this.state.notes.map((note, i) => {
return (
<Card
key={i}
className="__card"
thisApp={this}
currentNote={note}
/>
);
})}
</div>
</div>
</div>
);
}
}
I hope this is helpful for you!

Update list of displayed components on deletion in React

in the beginning on my path with React I'm creating simple to-do app where user can add/remove task which are basically separate components.
I create tasks using:
addTask(taskObj){
let tasksList = this.state.tasksList;
tasksList.push(taskObj);
this.setState({tasksList : tasksList});
}
I render list of components (tasks) using following method:
showTasks(){
return (
this.state.tasksList.map((item, index) => {
return <SingleTask
taskObj={item}
removeTask = {(id) => this.removeTask(id)}
key = {index}/>;
})
);
}
method to remove specific task takes unique ID of task as an argument and based on this ID I remove it from the tasks list:
removeTask(uID){
this.setState(prevState => ({
tasksList: prevState.tasksList.filter(el => el.id != uID )
}));
}
But the problem is, when I delete any item but the last one, it seems like the actual list of components is the same only different objects are passed to those components.
For example:
Lets imagine I have 2 created componentes, if I set state.Name = 'Foo' on the first one, and state.Name='Bar' on the second one. If I click on remove button on the first one, the object associated to this component is removed, the second one becomes first but it's state.Name is now 'Foo' instead of 'Bar'.
I think I'm missing something there with correct creation/removing/displaying components in react.
Edit:
Method used to remove clicked component:
removeCurrentTask(){
this.props.removeTask(this.props.taskObj.id);
}
SingleTask component:
class SingleTask extends Component{
constructor(props) {
super(props);
this.state={
showMenu : false,
afterInit : false,
id: Math.random()*100
}
this.toggleMenu = this.toggleMenu.bind(this);
}
toggleMenu(){
this.setState({showMenu : !this.state.showMenu, afterInit : true});
}
render(){
return(
<MDBRow>
<MDBCard className="singleTaskContainer">
<MDBCardTitle>
<div class="priorityBadge">
</div>
</MDBCardTitle>
<MDBCardBody className="singleTaskBody">
<div className="singleTaskMenuContainer">
<a href="#" onClick={this.toggleMenu}>
<i className="align-middle material-icons">menu</i>
</a>
<div className={classNames('singleTaskMenuButtonsContainer animated',
{'show fadeInRight' : this.state.showMenu},
{'hideElement' : !this.state.showMenu},
{'fadeOutLeft' : !this.state.showMenu && this.state.afterInit})}>
<a
title="Remove task"
onClick={this.props.removeTask.bind(null, this.props.taskObj.id)}
className={
classNames(
'float-right btn-floating btn-smallx waves-effect waves-light listMenuBtn lightRed'
)
}
>
<i className="align-middle material-icons">remove</i>
</a>
<a title="Edit title"
className={classNames('show float-right btn-floating btn-smallx waves-effect waves-light listMenuBtn lightBlue')}
>
<i className="align-middle material-icons">edit</i>
</a>
</div>
</div>
{this.props.taskObj.description}
<br/>
{this.state.id}
</MDBCardBody>
</MDBCard>
</MDBRow>
);
}
}
Below visual representation of error, image on the left is pre-deletion and on the right is post-deletion. While card with "22" was deleted the component itself wasn't deleted, only another object was passed to it.
Just to clarify, the solution was simpler than expected.
In
const showTasks = () => taskList.map((item, index) => (
<SingleTask
taskObj={item}
removeTask ={removeTask}
key = {item.id}
/>
)
)
I was passing map index as a key, when I changed it to {item.id} everything works as expected.
In short, in the statement tasksList.push(<SingleTask taskObj={taskObj} removeTask ={this.removeTask}/>);, removeTask = {this.removeTask} should become removeTask = {() => this.removeTask(taskObj.id)}.
However, I would reconsider the way the methods addTask and showTasks are written. While the way you have written isn't wrong, it is semantically unsound. Here's what I would do:
addTask(taskObj){
let tasksList = this.state.tasksList;
tasksList.push(taskObj);
this.setState({tasksList : tasksList});
}
showTasks(){
return (
this.state.tasksList.map((item, index) => {
return <SingleTask
taskObj={item}
removeTask ={() => this.removeTask(item.id)}/>;
})
);
}
const SingleTask = (task) => {
const { taskObj } = task;
return <div onClick={task.removeTask}>
{ taskObj.title }
</div>
}
// Example class component
class App extends React.Component {
state = {
tasksList: [
{ id: 1, title: "One" },
{ id: 2, title: "Two" },
{ id: 3, title: "Three" },
{ id: 4, title: "Four" }
]
}
addTask = (taskObj) => {
let tasksList = this.state.tasksList;
tasksList.push(taskObj);
this.setState({tasksList : tasksList});
}
showTasks = () => {
return (
this.state.tasksList.map((item, index) => {
return <SingleTask
key={index}
taskObj={item}
removeTask ={() => this.removeTask(item.id)}/>;
})
);
}
removeTask(id) {
this.setState(prevState => ({
tasksList: prevState.tasksList.filter(el => el.id != id )
}));
}
render() {
return (
<div className="App">
<div> {this.showTasks()} </div>
</div>
);
}
}
// Render it
ReactDOM.render(
<App />,
document.body
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Passing react function to children down, event.target empty

I'm struggling to grasp a react concept that to me is likely used all the time.
I have an app with a state.
I have a section below app.
Below section I have clickable tile that receives a function to update app status. This works, however the event.target appears to be null.
I'm passing the function to update the status all the way down from app as a prop.
How can I fix this / what am I missing?
import React, { Component } from 'react';
import './App.css';
const Section = ({ handleClick }) => {
return (
<div className="section">
Section
<Tile handleClick={handleClick} title="1" />
<Tile handleClick={handleClick} title="2" />
<Tile handleClick={handleClick} title="3" />
</div>
)
}
const Tile = ({ handleClick, title }) => {
return (
<div className="tile" onClick={handleClick}>
tile {title}
</div>
)
};
class App extends Component {
constructor(props) {
super(props);
this.state = {
modalOpen: false
};
}
openModal = () => {
this.setState({
modalOpen: true,
openedBy: ""
})
}
closeModal = (event) => {
this.setState({
modalOpen: false,
openedBy: event.target.title
})
}
render() {
return (
<div className="App">
<div>ModalOpen = {this.state.modalOpen.toString()}</div>
<div>Opened by = {this.state.openedBy}</div>
<Section handleClick={this.openModal}></Section>
<a href="#" onClick={this.closeModal}>Close modal</a>
</div>
);
}
}
export default App;
Thanks so much for pointer in the right direction!
You are not passing down a title prop to your Tile component, but your are passing down a number prop.
You can create a new function in the Tile component that calls the handleClick with the number, which you then use to set the openedBy in your App.
Example
const Section = ({ handleClick }) => {
return (
<div className="section">
Section
<Tile handleClick={handleClick} number="1" />
<Tile handleClick={handleClick} number="2" />
<Tile handleClick={handleClick} number="3" />
</div>
);
};
const Tile = ({ handleClick, number }) => {
return (
<div className="tile" onClick={() => handleClick(number)}>
tile {number}
</div>
);
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
modalOpen: false,
openedBy: ""
};
}
openModal = title => {
this.setState({
modalOpen: true,
openedBy: title
});
};
closeModal = () => {
this.setState({
modalOpen: false,
openedBy: ""
});
};
render() {
return (
<div className="App">
<div>ModalOpen = {this.state.modalOpen.toString()}</div>
<div>Opened by = {this.state.openedBy}</div>
<Section handleClick={this.openModal} />
<a href="#" onClick={this.closeModal}>
Close modal
</a>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
It seems to be working perfectly fine :
const Section = ({ handleClick }) => {
return (
<div className="section">
Section
<Tile handleClick={handleClick} number="1" />
<Tile handleClick={handleClick} number="2" />
<Tile handleClick={handleClick} number="3" />
</div>
)
}
const Tile = ({ handleClick, title }) => {
return (
<div className="tile" onClick={handleClick}>
tile {title}
</div>
)
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
modalOpen: false
};
}
openModal = event => {
console.log(event.target)
this.setState({
modalOpen: true,
openedBy: ""
})
}
closeModal = event => {
console.log(event.target)
this.setState({
modalOpen: false,
openedBy: event.target.title
})
}
render() {
return (
<div className="App">
<div>ModalOpen = {this.state.modalOpen.toString()}</div>
<div>Opened by = {this.state.openedBy}</div>
<Section handleClick={this.openModal}></Section>
<a href="#" onClick={this.closeModal}>Close modal</a>
</div>
);
}
}
ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.5.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.5.2/umd/react-dom.production.min.js"></script>
<div id='root'>
However I assume that you want to pass down the title of the clicked element back into the handler. If so, I recommend using a curried function, with 2 sets of parameters, and setting the title variable as the first one :
openModal = title => event => {
console.log('Opened by : ', title, event.target)
this.setState({
modalOpen: true,
openedBy: ""
})
}
Your Tile component can now indicate which title it has by calling the function the first time :
const Tile = ({ handleClick, title }) => {
return (
<div className="tile" onClick={handleClick(title)}>
tile {title}
</div>
)
};
Working example :
const Section = ({ handleClick }) => {
return (
<div className="section">
Section
<Tile handleClick={handleClick} title="1" />
<Tile handleClick={handleClick} title="2" />
<Tile handleClick={handleClick} title="3" />
</div>
)
}
const Tile = ({ handleClick, title }) => {
return (
<div className="tile" onClick={handleClick(title)}>
tile {title}
</div>
)
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
modalOpen: false
};
}
openModal = title => event => {
console.log('Opened by : ', title)
this.setState({
modalOpen: true,
openedBy: ""
})
}
closeModal = event => {
this.setState({
modalOpen: false,
openedBy: event.target.title
})
}
render() {
return (
<div className="App">
<div>ModalOpen = {this.state.modalOpen.toString()}</div>
<div>Opened by = {this.state.openedBy}</div>
<Section handleClick={this.openModal}></Section>
<a href="#" onClick={this.closeModal}>Close modal</a>
</div>
);
}
}
ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.5.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.5.2/umd/react-dom.production.min.js"></script>
<div id='root'>

How to pass ref of component up three levels for scroll event?

I am using the <InfiniteScroll/> component in my code like this:
<div style="height:700px;overflow:auto;" ref={(ref) => this.scrollParentRef = ref}>
<div>
<InfiniteScroll
pageStart={0}
loadMore={loadFunc}
hasMore={true || false}
loader={<div className="loader" key={0}>Loading ...</div>}
useWindow={false}
getScrollParent={() => this.scrollParentRef}
>
{items}
</InfiniteScroll>
</div>
</div>
I need to pass the from ref = {(ref) => this.scrollParentRef = ref; } to the component's grandparent, however my code does not work as expected. Here is my full component source code:
class AppEmpty extends Component {
constructor(props) {
super(props);
}
componentDidMount(){
this.props.setReferences(this.scrollParentRef);
}
render() {
const { children } = this.props;
const isActive = this.props.isActive;
return (
<div className="App">
<NavBar/>
<MenuRight/>
<div id="content"
className={isActive ? "content_enable": ""}
style={this.props.menuRight.styleContent}
ref={ (ref) => this.scrollParentRef = ref }
>
<FlashMessages/>
{children}
</div>
</div>
);
}
}
AppEmpty.protoTypes = {
children: PropTypes.element.isRequired,
};
function mapsStateToProps(state){
return {
menuRight: state.menuRight,
isActive: state.isActive,
};
}
export default connect (mapsStateToProps, {setReferences})(AppEmpty);
The documentation says:
getScrollParent Function Override method to return a different scroll listener if it's not the immediate parent of InfiniteScroll.
Can someone suggest a way to pass that reference in an easy way to the <InfiniteScroll/>?
After trying everything, I realized that I can pass as a refs using document.getElementById ('comp_id_a_referenciar') :(
Content-> compomente_base -> form_busqueda, Tables -> InfiniteScroll(Refs_Content).
<div>
<InfiniteScroll
pageStart={0}
loadMore={loadFunc}
hasMore={true || false}
loader={<div className="loader" key={0}>Loading ...</div>}
useWindow={false}
getScrollParent={ () => document.getElementById('content') }
>
{items}
</InfiniteScroll>
working! nice!

Props Parent component to a parent - child component

How can I can call a function that is in the parent component (App) from a child component (Card)?
Card component renders one item (CHILD)
const Card = (props) => {
return (
<div style={{margin: '1em'}}>
<img alt="Profile" width="75" src={props.avatar_url} />
<div style={{display: 'inline-block', marginLeft: 10}}>
<div style={{fontSize: '1.25em', fontWeight: 'bold'}}>
{props.name}
</div>
<div>{props.company}</div>
<button
//when I click here should trigger App's delete func who deletes the select item.
onClick = { () => alert()}
className="btn btn-danger btn-sm">Delete</button>
</div>
</div>
);
};
CardList component renders a list of item (CHILD-PARENT)
const CardList = (props) => {
return (
//Props.nameProp = Value {...Card} spred operator
<div>
{props.cards.map(card => <Card key={card.id} {...card} />)}
</div>
);
}
Parent component:
class App extends React.Component {
state = {
cards: [
]
};
addNewCard = (cardInfo) => {
this.setState(prevState => ({
cards: prevState.cards.concat(cardInfo)
}))
};
deleteCard = (selectedCard) => {
this.setState(prevState => ({
//array.filter creates a new array with elements who pass the foo
cards: prevState.cards.filter(card => card !== selectedCard)
}));
}
render(){
const {
cards,
} = this.state;
return(
<div className="container" style={{marginTop: 15}}>
<Form onSubmit={this.addNewCard} />
<div className="container" style={{padding: 20}}>
{
cards.length > 0 ?
<CardList cards={cards} />
:
<p>Your list is empty</p>
}
</div>
</div>
);
}
}
EXPLANATION
I want to delete a item who is in the state list but that list is in the parent component, how can I do that?
You can pass down the deleteCard method as a prop to CardList, and from CardList to each Card component and call it there.
Example
const Card = props => (
<button onClick={() => props.deleteCard(props.card)}>{props.card}</button>
);
const CardList = props => (
<div>
{props.cards.map(card => (
<Card card={card} deleteCard={props.deleteCard} />
))}
</div>
);
class App extends React.Component {
state = {
cards: [1, 2, 3]
};
deleteCard = card => {
this.setState(({ cards }) => ({
cards: cards.filter(element => element !== card)
}));
};
render() {
const { cards } = this.state;
return <CardList cards={cards} deleteCard={this.deleteCard} />;
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

Resources