I'm mostly a C++ developer and I'm having a hard time understanding how can I create something as a 'Delegate' in React.
What I wanna achieve: Pass a custom component to a Table Component that has the required code to edit the data on a table cell correctly.
on my mainApp:
<TableComponent
headerData=["first", "second", "third"]
rowEditDelegates=[<input/>, <button></button>, <combobox/>]
/>
The code is much shorter for brievety.
class TableComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
editDelegates = this.props.rowEditDelegates;
displayEditors = false;
}
onEditToogled() {
/* Here I have no idea how to get the editDelegates
and pass data to it. */
setState({displayEditors : true});
}
}
render() {
let row = null;
if(this.state.displayEditors) {
row = this.state.editDelegates.map((item) => <td> {item} </td>)
} else {
row = this.props.bodyData.map((item) => <td> {item} </td>)
}
}
};
I cannot access the delegate's methods because it's a rendered component, and I didn't understand how to work with a component "pointer" (I don't even know if it exists), maybe my problem needs a different mindset than a c++ programmer one.
You have couple of options here:
1) Use cloneElement function:
onEditToogled()
{
setState({displayEditors : true});
}
render()
{
let row = null;
if(this.state.displayEditors)
{
row = this.state.editDelegates.map((item) => {
var alteredElement = React.cloneElement(item, {className: "newPropertyValue"});
return (<td> {alteredElement} </td>);
});
}
else
{
row = this.props.bodyData.map((item) => <td> {item} </td>)
}
}
2) Change the way how you pass the editor components, for example:
<TableComponent
headerData=["first", "second", "third"]
rowEditDelegates=[(props) => <input {...props}/>, (props) => <button {...props}/>, (props) => <combobox {...props}/>]
/>
And later:
render() {
let row = null;
if(this.state.displayEditors) {
row = this.state.editDelegates.map((itemFunc) => <td> {itemFunc({className: "newPropertyValue"})} </td>)
} else {
row = this.props.bodyData.map((itemFunc) => <td> {itemFunc()} </td>)
}
}
As a side note.
I do not think there is a need for you to copy and keep your "delegates" in state. They are unlikely to change? If so - just leave them in props.
Related
I am trying to create a simple table using ReactJS to display user information. Here's how the general code structure looks like:
class ParentComponent extends React.Component {
state = {
data : []
}
componentDidMount() {
// initializes state with data from db
axios.get("link/").then(res => {
this.setState({data: res.data});
});
// I should be able to call this.getData() instead
// of rewriting the axios.get() function but if I do so,
// my data will not show up
}
// retrieves array of data from db
getData = () => {
axios.get("link/").then(res => {
this.setState({data: res.data});
});
}
render() {
return (
<div>
<ChildComponent data={this.state.data} refetch={this.getData} />
</div>
)
}
}
Each of the generated rows should have a delete function, where I'll delete the entry from the database based on a given id. After the deletion, I want to retrieve the latest data from the parent component to be redisplayed again.
class ChildComponent extends React.Component {
// deletes the specified entry from database
deleteData = (id) => {
axios.get("deleteLink/" + id).then(res => {
console.log(res);
// calls function from parent component to
// re-fetch the latest data from db
this.props.refetch();
}).catch(err => {console.log(err)});
}
render() {
let rows = null;
if(this.props.data.length) {
// map the array into individual rows
rows = this.props.data.map(x => {
return (
<tr>
<td>{x.id}</td>
<td>{x.name}</td>
<td>
<button onClick={() => {
this.deleteData(x.id)
}}>
Delete
</button>
</td>
</tr>
)
})
}
return (
<div>
<table>
<thead></thead>
<tbody>
{rows}
</tbody>
</table>
</div>
)
}
}
The two problems which I encountered here are:
Logically, I should be able to call this.getData() from within componentDidMount(), but if I do so, the table doesn't load.
Whenever I try to delete a row, the table will not reflect the update even though the entry is removed from the database. The table will only be updated when I refresh the page or delete another row. Problem is, the component is always lagging behind by 1 update.
So far I have tried:
this.forceUpdate() - doesn't work
this.setState({}) - empty setState doesn't work either
changing componentDidMount() to componentDidUpdate() - error showing that I have "reached maximum depth" or something along that line
adding async await in front of axios - doesn't work
Any help is appreciated. Thanks in advance.
EDIT: I did some debugging and tracked down the issue, which is not relevant to my question. My deleteData() which is located in ChildComponent uses axios.post() instead of axios.get(), which I overlooked.
deleteData = (id) => {
axios.post("deleteLink/", id).then(res => {
console.log(res);
// calls function from parent component to
// re-fetch the latest data from db
this.props.refetch();
}).catch(err => {console.log(err)});
}
In order for axios.post() to return a response, in order to perform .then(), you'll need to add a res.json() to the routing codes.
You should map data into your child.
Change your parent like this:
class ParentComponent extends React.Component {
state = {
data : []
}
componentDidMount() {
this.getData();
}
getData = () => axios.get("link/").then(res => this.setState({data: res.data});
deleteData = (id) => axios.get("deleteLink/" + id).then(res => this.getData())
.catch(err => { console.log(err) });
render() {
return (
<div>
<table>
<thead></thead>
<tbody>
{this.state.data.map(x => <ChildComponent row={x} deleteData={this.deleteData} />)}
</tbody>
</table>
</div>
)
}
}
And your child component should be like this
const ChildComponent = ({row,deleteData}) => (
<tr>
<td>{row.id}</td>
<td>{row.name}</td>
<td><button onClick={() => deleteData(row.id)}>Delete</button></td>
</tr >
)
I can't find an issue in your code, the only way I can help is to tell you how I would debug it.
edit parent like so:
class ParentComponent extends React.Component {
state = {
data : []
}
componentDidMount() {
// You are right when you say this should works, so
// stick to it until the bug is fixed
console.log("parent mounted");
this.getData();
}
// retrieves array of data from db
getData = () => {
console.log("fetching data");
axios.get("link/").then(res => {
console.log("fetched data", res.data);
this.setState({data: res.data});
});
}
render() {
return (
<div>
<ChildComponent
data={this.state.data}
refetch={this.getData}
/>
</div>
)
}
}
and in the child component add these 2 lifecycle methods just for debugging purposes:
componentDidMount () {
console.log("child mounted", this.props.data)
}
componentDidUpdate (prevProps) {
console.log("old data", prevProps.data);
console.log("new data", this.props.data);
console.log("data are equal", prevProps.data === this.props.data);
}
if you can share the logs I can try help you more
I am working on a quiz component, where user can appear for test. Questions are shown one after another to user and user checks the right answer.
But I am facing the below issue.
Description:
Checkbox does not uncheck for next question. It remains checked, once user click on any of the checkbox.
Steps:
1. Click on any checkbox options for the question.
2. Click on next for next question. [checkbox is checked from previous question]
[]2
Expected:
When next question appears, the checkbox should not be checked.
Actual:
When next questions appears, the checkbox is checked.
Code: On click of next, this component gets its data as a props from parent component.
// This component show one question at a time
import React from 'react';
import TextEditorDisplay from '../../texteditor/TextEditorDisplay';
import Form from 'react-bootstrap/Form';
class TestComponent extends React.PureComponent {
handleCheck = (e, idx) => {
console.log('inside handleCheck',e.target.value)
this.props.setAnswerGivenByUser(idx, e.target.checked);
};
render() {
return (
<div className="container">
<h3 className="quiz-question">
<TextEditorDisplay editorContent={this.props.quizQuestion.question} />
</h3>
<Form>
<table>
<tbody>
{this.props.quizQuestion.options && this.props.quizQuestion.options.map((option, idx) => (
<tr key={idx}>
<td>
<Form.Group controlId="formBasicCheckbox">
<Form.Check type="checkbox" value={option.data} onChange={e => this.handleCheck(e, idx)}/>
</Form.Group>
</td>
<td>
<p key={idx}>{option.data}</p>
</td>
</tr>
))}
</tbody>
</table>
</Form>
</div>
);
}
}
export default TestComponent;
Parent component:
import React from 'react';
import TestComponent from '../components/skill-assessment/users/TestComponent';
import Button from 'react-bootstrap/Button';
import api from '../services/remote/api';
class TestHomePage extends React.PureComponent {
x = 0;
y = 0;
arr = [];
constructor(props) {
super(props);
this.getQuizQuestionsAsPerLevel = this.getQuizQuestionsAsPerLevel.bind(this);
this.state = {
quizQuestion: [],
show: true,
options: [],
answers: []
};
}
getIdFromUrl = () => {
var url = this.props.location.pathname;
var splitUrl = url.split('/');
return splitUrl[2].toString();
};
componentDidMount() {
this.getQuizQuestionsAsPerLevel(1);
this.getQuizQuestionsAsPerLevel(2);
this.getQuizQuestionsAsPerLevel(3);
this.getQuizQuestionsAsPerLevel(4);
this.getQuizQuestionsAsPerLevel(5);
this.getQuizQuestionsAsPerLevel(6);
console.log('component did mount arr', this.arr);
}
getQuizQuestionsAsPerLevel(level) {
try {
api.getQuizQuestionsAsPerLevel({ id: this.getIdFromUrl(), level: level }).then(response => {
this.arr.push(response.data);
console.log('arr inside api', this.arr);
});
} catch (exception) {
console.log('exception', exception);
}
}
addUserQandA() {
try {
api.addUserQandA({
quizId: this.getIdFromUrl(),
quizQandA: [{ quizQuestionId: this.state.quizQuestion._id }, { answers: this.state.answers }]
}).then(response => {
console.log('add QandA response', response);
});
} catch (exception) {
console.log('exception', exception);
}
}
nextQuestion = () => {
// send prev Question data to QandA
if (this.state.quizQuestion && this.state.answers) {
this.addUserQandA();
}
if (this.x < this.arr.length - 1 && this.y >= this.arr[this.x].length) {
this.x = this.x + 1;
this.y = 0;
this.setState({ quizQuestion: this.arr[this.x][this.y], answers: [] });
} else if (this.x < this.arr.length && this.y < this.arr[this.x].length) {
this.setState({ quizQuestion: this.arr[this.x][this.y] });
this.y = this.y + 1;
} else {
// hide next button and highlight submit button
this.setState({ show: false });
}
};
setAnswerGivenByUser = (answerId, shouldAdd) => {
const answers = this.state.answers.slice();
if (shouldAdd) {
if (!answers.includes(answerId)) {
answers.push(answerId);
}
} else {
if (answers.includes(answerId)) {
const answerIndex = answers(a => a === answerId);
answers.splice(answerIndex, 1);
}
}
this.setState({ answers });
};
render() {
console.log('answers', this.state.answers);
return (
<div className="container">
<TestComponent quizQuestion={this.state.quizQuestion} setAnswerGivenByUser={this.setAnswerGivenByUser} />
{this.state.show && (
<Button variant="primary" onClick={this.nextQuestion}>
Next
</Button>
)}
<Button variant="primary">Submit</Button>
</div>
);
}
}
export default TestHomePage;
quiz Data Strcuture
Not refreshing problem was caused by not forced rerenderings.
For each levels <tr /> elements was rendered with numbered key always starting from 0. This way next level renders was besed on exiting nodes (updating), not rendered as new ones. First not changed node (in sense of the same props) stops deeper analisys. In this case it stops on <Form.Group controlId="formBasicCheckbox"> - it's child is not updated even when option.data differs.
Solution
key is used for distinguish nodes rendered in loops. key should be unique. It should not be a number only ... they should be always unique.
Simple fix is to use additionally passed prop level:
<tr key={this.props.level * 10 + idx} />
In fact ... as <p key={idx}>{option.data}</p> was updated ... it should be enough to use this (or similar) unique key/prop for <Form.Group/> (f.e. controlId). Using unique on <tr/> level we're forcing render of a new structure (can be costly in some scenarios).
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.
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>
)
}
}
I currently have something like this:
const socket = require('socket.io-client')('https://example.com');
(....)
// Listen to the channel's messages
socket.on('m', message => {
// this is a Redux action that updates the state
this.props.updateTrades(message);
});
The reducer looks like this:
case actions.UPDATE_TRADES:
return {
...state,
trades: [
...state.trades,
action.trade
]
};
I've tried not using redux and just to the following:
socket.on('m', message => {
this.setState(state => {
if (state.trades.length > 99) {
state.trades.splice(0, 1);
}
return {
trades: [
...state.trades,
message
]
});
});
I don't need to keep increasing my trades array. I'm happy just to keep around 100 items or so...
Socket is sending around 15 messages / second.
My problem is: I can't seem to render the messages in real-time! It just freezes. I guess the stream is just too fast? Any suggestions?
Thanks in advance!
The thing is to do the minimum possible and when the trades change only draw what has change and not all of the elements of the array.A technique that I use is to keep a cache map of already drawn obj, so in the render method I only render the new incoming elements.
Take a look at https://codesandbox.io/s/wq2vq09pr7
class RealTimeList extends React.Component {
constructor(props) {
super(props);
this.cache = [];
}
renderRow(message, key) {
return <div key={key}>Mesage:{key}</div>;
}
renderMessages = () => {
//let newMessages=this,props.newMessage
let newElement = this.renderRow(this.props.message, this.cache.length);
this.cache.push(newElement);
return [...this.cache];
};
render() {
return (
<div>
<div> Smart List</div>
<div className="listcontainer">{this.renderMessages()}</div>
</div>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = { message: "hi" };
}
start = () => {
if (this.interval) return;
this.interval = setInterval(this.generateMessage, 200);
};
stop = () => {
clearTimeout(this.interval);
this.interval = null;
};
generateMessage = () => {
var d = new Date();
var n = d.getMilliseconds();
this.setState({ title: n });
};
render() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={this.start}> Start</button>
<button onClick={this.stop}> Stop</button>
<RealTimeList message={this.state.message} />
</div>
);
}
}
The class RealTime List have a cache of elements.Let me know if this helps.
It's probably not a good idea to try to render all of the changes. I think you should try rendering them in batches so you only update once every few seconds, that should help.