React: Changing parent state not rendering child - reactjs

I am extremely new to react and am building a simple todo list app. I am trying to edit data from my child component and send it back to my parent. When I am printing console logs the parent state seems to be getting set correctly, but the child elements are not refreshing. Am I doing something conceptually wrong here?
I have tried to share the entire code as I am not sure whether it is correct conceptually. I am new to JS. When the handleSave() and handleComplete() are called i can see correct values getting returned and set to my PArent State, but there is no refresh of the child components.
Below is my Parent class code.
class App extends Component {
state = {
taskList: [
]
};
saveEventHandler = data => {
console.log("I am in saveEventHandler");
var uniqid = Date.now();
const taskList = [...this.state.taskList];
taskList.push({
id: uniqid,
taskDescText: data,
isFinished: false
});
console.log(taskList);
this.setState({'taskList':taskList});
};
deleteEventHandler = (index) => {
const taskList = [...this.state.taskList];
taskList.splice(index,1)
this.setState({'taskList':taskList});
}
editEventHandler = (index,data) => {
var uniqid = Date.now();
console.log("In edit event handler")
console.log(data)
console.log(index)
const taskList = [...this.state.taskList];
taskList[index] = {
id: uniqid,
taskDescText: data,
isFinished: false
}
this.setState({'taskList':taskList});
console.log(this.state.taskList)
}
handleComplete = (index) => {
console.log("In complete event handler")
const taskList = [...this.state.taskList];
const taskDescriptionOnEditIndex = taskList[index]
taskDescriptionOnEditIndex.isFinished = true
taskList[index] = taskDescriptionOnEditIndex
this.setState({'taskList':taskList});
console.log(this.state.taskList)
}
render() {
return (
<div className="App">
<h1>A Basic Task Listing App </h1>
<CreateTask taskDescription={this.saveEventHandler} />
{this.state.taskList.map((task, index) => {
return (
<Task
taskDescText={task.taskDescText}
taskCompleted={task.isFinished}
deleteTask={() => this.deleteEventHandler(index)}
editTask={(editTask) => this.editEventHandler(index,editTask)}
handleComplete={() => this.handleComplete(index)}
editing='false'
/>
);
})}
</div>
);
}
}
export default App;
and my child class code
export default class Task extends React.Component {
state = {
editing : false
}
notCompleted = {color: 'red'}
completed = {color: 'green'}
textInput = React.createRef();
constructor(props) {
super(props);
this.state = props
}
handleEdit = () => {
this.setState({editing:true});
}
handleSave = () => {
this.props.editTask(this.textInput.current.value);
this.setState({editing:false});
};
editingDiv = (<div className = 'DisplayTask'>
<span className='TaskDisplayText' style={!this.props.taskCompleted ? this.notCompleted: this.completed}>{this.props.taskDescText} </span>
<button label='Complete' className='TaskButton' onClick={this.props.handleComplete}> Complete</button>
<button label='Edit' className='TaskButton' onClick={this.handleEdit}> Edit Task</button>
<button label='Delete' className='TaskButton' onClick={this.props.deleteTask}> Delete Task</button>
</div> );
nonEditingDiv = ( <div className = 'DisplayTask'>
<input className='TaskDescEditInput' ref={this.textInput}/>
<button label='Save' className='TaskButton' onClick={this.handleSave} > Save Task</button>
<button label='Delete' className='TaskButton' onClick={this.props.deleteTask}> Delete Task</button>
</div>);
render() {
return (
!this.state.editing ? this.editingDiv : this.nonEditingDiv
)
};
}

Move your editingDiv and nonEditingDiv definitions inside render() method. Since you're defining them as instance variables, they're initialized once, and never get re-rendered again with new prop values.
By moving them to render() method, render() which is called every time when there's a prop update will pick up the new prop values.

Related

Should I pass a class to my (React) component, and keep my state in that component

I'm creating an application that receives a JSON object I need to visualise to an editor to edit this JSON and POST back. I transform the JSON to classes as soon as I receive the code. The classes have methods to edit the items.
At this moment I'm using React, which is new for me, to visualise the JSON as an editor. I always pass the classes I create to the components and use methods on those classes to update the state. The classes are my single source of truth for the data.
I wonder if this is a good approach in the world of frontend component based development.
What I do in the example of a todoList:
I receive a JSON object with a todolist name and an array of todos which I convert to a class:
const json = {
"title": "My todolist",
"items": [
{"title": "todo 1", done: false}
]
}
becomes
class TodoList {
constructor(name, items) {
this.name = name;
this.items = items.map(item => return new TodoItem(item) )
}
}
class TodoItem {
constructor(title, done) {
this.title = title;
this.done = done;
}
markDone() {
this.done = true;
}
}
```
Now when I print my todoitem + button to mark it done I do something like this:
```
export function TodoItem(props) {
const todoItem = props.todoItem; // this is the TodoItem class
const [item2, setItem] = React.useState({});
React.useEffect(() => {
setItem(todoItem);
});
return (
<div>
<h1>{item2.title}, done: {item2.done ? 'true' : 'false'}</h1>
<button onClick={(e) => {
e.preventDefault();
item2.markDone();
setItem({...item2});
}}>Mark as done</button>
</div>
);
}
```
Full example here: https://playcode.io/1165575
The data in the class gets updated and on a later moment I can convert the classes back to JSON and send it back.
**I wonder if this is a good approach in the world of frontend component based development.**
You should only pass to TodoItem the necessary props to render + a callback to update the state. Let me show you:
const TodoList = () => {
const [items, setItems] = useState([]);
return(
{items.map((item, index) => (
<TodoItem
title={item.title}
done={item.done}
onChange={newValue => {
let newItems = items;
newItems[index].done = newValue;
setItems(newItems);
}}
/>
))}
);
};
const TodoItem = ({ title, done, onChange }) => {
return (
<div>
<h1>{title}, done: {done}</h1>
<button onClick={() => {
onChange(!done);
}}>Mark as {done? 'undone' : 'done'}</button>
</div>
);
};

Pass child data to parent on an onClick event triggered in parent in React

I'm trying to pass child data to parent on an onClick event that happens in a parent button.
I have a basic mock defined below to explain what I'm trying to achieve.
const Parent = () => {
const childData = (data) => {
console.log(data);
}
const receiveData = () => {
childData();
}
return (
<>
<button onClick={receiveData}>Receive Child Data</button>
<Child onParentClick={childData} />
</>
)
}
const Child = ({onParentClick}) => {
// Trigger onParentClick here to pass some data to parent
return (
// something
)
}
How can I achieve this?
class Parent extends Component {
passDataToParent = data => {
console.log(data);
}
render() {
return (
<Child passDataToParent={this.passDataToParent} />
);
}
}
const Child = props => {
buttonClick = () => {
const data = {
a: 1
};
props.passDataToParent(data);
};
return (
<Button onClick={buttonClick} />
)
}
You can do something like this with callbacks (this example is using hooks)..
Edit: this does feel like an 'anti-pattern' to me though... It has been my understanding that React is meant for one way data flow from the top down.. (eg. why this is so tricky to accomplish, use of 'higher order components', etc..).. Furthermore, this doesn't necessarily "pass" data up to the parent.. it would be more appropriate to say 'a reference to a child function is being passed to the parent'.. the parent just calls the child function. Meaning "data" is not passed, rather the function itself.. Please correct me if I am wrong... I am fairly new to React. Finally, I do not understand the use case for something like this -- in which scenario does one need to use this "paradigm"?
[CodePen Mirror]
// Parent
const Parent = () => {
let child;
function getChildData(callbacks){
child = callbacks;
}
return (
<div>
<button onClick={() => child.getData()}>Retreive Child Data</button>
<Child onRetreiveChildData={callbacks => getChildData(callbacks)} />
</div>
);
}
// Child
const Child = ({ onRetreiveChildData }) => {
const [state, setState] = React.useState({
data: "Default Child Data"
});
if (onRetreiveChildData) {
onRetreiveChildData({
getData: () => getData("Data from Child: " + Math.random())
});
}
function getData(data){
setState({ data: data });
};
return (
<pre>{state.data}</pre>
)
};
ReactDOM.render(<Parent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id='root' style='width: 100%; height: 100%'>
</div>

Reactjs show hide multiple components

Newbie React question here on show hide functionality.
I have a state of 'show' that I set to false:
this.state = {
show: false,
};
Then I use the following function to toggle
toggleDiv = () => {
const { show } = this.state;
this.setState({ show : !show })
}
And my display is
{this.state.show && <xxxxxx> }
This all works fine. However I want to apply the function it to multiple cases (similar to accordion, without the closing of other children. So I change my constructor to
this.state = {
show: [false,false,false,false,false,false]
};
and this to recognise there are 6 different 'shows'.
{this.state.show[0] && <xxxxxx> }
{this.state.show[1] && <xxxxxx> } etc
But where I get stuck is how to account for them in my toggleDiv function. How do I insert the square bracket reference to the index of show (if this is my problem)?
toggleDiv = () => {
const { show } = this.state;
this.setState({ show : !show })
}
Thanks for looking.
First of all I'd suggest you not to rely on current state in setState function, but to use the callback option to be 100% sure that you are addressing to the newest state:
this.setState((prevState) => ({ show: !prevState.show }));
How to deal with multiple elements?
You'll have to pass the index of currently clicked element.
{yourElements.map((elem, i) => <YourElem onClick={this.toggleDiv(i)} />)}
and then inside your toggleDiv function:
toggleDiv = (i) => () => {
this.setState((prevState) => {
const r = [...prevState.show]; // create a copy to avoid state mutation
r[i] = !prevState.show[i];
return {
show: r,
}
}
}
Use an array instead of a single value. In your toggle div function make a copy of the state array make necessary changes and push the entire array back up to state at the end.
This is some simplified code showing the workflow I described above
export default class myClass extends React.Component{
constructor(props){
super(props);
this.state = { show: new Array(2).fill(false) };
}
//you need a index or id to use this method
toggleDiv = (index) => {
var clone = Object.assign( {}, this.state.show ); //ES6 Clones Object
switch(clone[index]){
case false:
clone[index] = true
break;
case true:
clone[index] = false
break;
}
this.setState({ show: clone });
}
render(){
return(
<div>
{ this.state.show[0] && <div> First Div </div> }
{ this.state.show[1] && <div> Second Div </div> }
{ this.state.show[2] && <div> Third Div </div> }
</div>
)
}
}

How to open dialog from a function in react?

I am currently using this code to create my scheduler application.
This is my code now - Scheduler.js
class CalendarScheduler extends Component {
state = {
viewModel: schedulerData,
showBookingDialog: true,
}
handleClickOpen = () => {
this.setState( (prevState) => {
return {showBookingDialog : !prevState.showBookingDialog};
} )
};
handleClose = () => {
this.setState({ showBookingDialog: false });
};
render() {
const { viewModel } = this.state;
let schedulerData = this.state.viewModel;
schedulerData.setResources(this.props.rooms);
return (
<div>
<Scheduler schedulerData={viewModel}
prevClick={this.prevClick}
nextClick={this.nextClick}
onSelectDate={this.onSelectDate}
newEvent={this.newEvent}
/>
</div>
)
}
In Scheduler.js, I have a function for newEvent:
newEvent = (schedulerData, slotId, slotName, start, end, type, item) => {
return(
<BookingDialog
open={this.state.showBookingDialog}
onClose={this.handleClose}
/>
);
}
My question is why when I click on the scheduler to create a new event, it doesnt open up my BookingDialog?
The previous newEvent function uses window.confirm, which can be found here in line 93.
You can set a state variable in your Scheduler component to show BookingDialog, And in newEvent function make that state variable as 'true'.And in scheduler component, view the BookingDialog based on this state variable.
newEvent = (schedulerData, slotId, slotName, start, end, type, item) => {
this.setState({showBookingDialog:true})
}
and in Scheduler component
{this.state.showBookingDialog && <BookingDialog
open={this.state.showBookingDialog}
onClose={this.handleClose}
/>}

How to open a page in new tab on click of a button in react? I want to send some data to that page also

I'm working on a raise invoice page, in which user can raise a invoice on clicking of a button, I would call a api call and after getting the response I want to send some data to a page(RaisedInvoice.jsx) which should open in a new tab, how can i do it. The thing which I am not getting is how to open a page in new tab on click of a button in ReactJs.
RaiseInvoice.jsx:
import React from 'react';
import Links from './Links.jsx';
import history from './history.jsx';
import axios from 'axios';
class RaiseInvoice extends React.Component {
constructor(props) {
super(props);
// This binding is necessary to make `this` work in the callback
this.state = {projects: [], searchParam : ''};
this.raiseInvoiceClicked = this.raiseInvoiceClicked.bind(this);
}
raiseInvoiceClicked(){
// here i wish to write the code for opening the page in new tab.
}
render() {
return (
<div>
<Links activeTabName="tab2"></Links>
<div className="container">
<div className = "row col-md-4">
<h1>Raise Invoice...</h1>
</div>
<div className = "row col-md-4"></div>
<div className = "row col-md-4" style ={{"marginTop":"24px"}}>
<button type="button" className="btn btn-default pull-right" onClick={this.raiseInvoiceClicked}>Raise Invoice</button>
</div>
</div>
</div>
)
}
}
export default RaiseInvoice;
Since you were going to send big data, appending them to your target URL looks shabby. I would suggest you use 'LocalStorage' for this purpose. So your code looks like this,
raiseInvoiceClicked(){
// your axios call here
localStorage.setItem("pageData", "Data Retrieved from axios request")
// route to new page by changing window.location
window.open(newPageUrl, "_blank") //to open new page
}
In your RaisedInvoice.jsx, retrieve the data from Local Storage like this,
componentWillMount() {
localStorage.pagedata= "your Data";
// set the data in state and use it through the component
localStorage.removeItem("pagedata");
// removing the data from localStorage. Since if user clicks for another invoice it overrides this data
}
You can just use plain JS to do it and append some query perimeters with it
raiseInvoiceClicked(){
const url = 'somesite.com?data=yourDataToSend';
window.open(url, '_blank');
}
Instead of calling raiseInvoiceClicked() function inside onclick method, you can try
onClick="window.open('your_url')"
in your code.
Simply do this!
const openLinkInNewTab = ( url ) => {
const newTab = window.open(url, '_blank', 'noopener,noreferrer');
if ( newTab ) newTab.opener = null;
}
//...
return (
//...
<button onClick={ () => openLinkInNewTab('your.url')}> Click Here </button>
//...
)
You can open it in a new window using the following code.
Please note that for props you can pass any child components that should be rendered inside new window.
const RenderInWindow = (props) => {
const [container, setContainer] = useState(null);
const newWindow = useRef(null);
useEffect(() => {
// Create container element on client-side
setContainer(document.createElement("div"));
}, []);
useEffect(() => {
// When container is ready
if (container) {
// Create window
newWindow.current = window.open(
"",
"",
"width=600,height=400,left=200,top=200"
);
// Append container
newWindow.current.document.body.appendChild(container);
// Save reference to window for cleanup
const curWindow = newWindow.current;
// Return cleanup function
return () => curWindow.close();
}
}, [container]);
return container && createPortal(props.children, container);
};
Pass this data with props:
let href = '...url_to_redirect...'; let data_to_send = [...];
let api_href = '...url_api_where_sent_data.../?data_to_send';
export const DictDefaultOptions = (url=(api_href), method='GET') => {
let defaultOptions = {
url : url,
method : method,
mode : 'cors',
headers : {'Access-Control-Allow-Origin':'*'}
};
return defaultOptions };
let sentData = {
method: defaultOptions.method,
mode: defaultOptions.mode
};
send_data_to_api = () => {
let api_return = await fetch(api_href, sentData)
.then(response => response.json())
.then(responseText => {
data = (JSON.parse(JSON.stringify(responseText)))
})
.catch(error => {
console.log(`${requestURL} error: `, error)
});
do { await this.wait(100) } while(Object.keys(api_return).length <= 0);
if (Object.keys(api_return).length > 0) {
return window.open(href, "_blank")
}
};

Resources